线程安全
定义
线程安全描述的是代码在并发执行下的正确性。按常见的表述,如果一个函数或函数库在多线程环境中被调用时,能够正确处理各线程之间的公用变量,使程序功能正确完成,就称其具备线程安全性[1]。另一种归纳是:当函数可被多个线程同时调用或访问,而不引发意外行为、竞态条件或数据损坏时,该函数即为线程安全[2]。
定义的核心是正确性。判断一个类是否线程安全,要放在单线程语义仍然成立的前提下:无论运行时如何调度、线程如何交错,也不需要在调用方追加同步措施,其行为都不偏离规格说明[3]。因此线程安全不是对象自身的孤立属性,而与它是否被多个线程共享、以何种方式被使用有关[4]。
同一种保证在不同厂商的文档中措辞并不统一,实践中常按强弱分级。较通用的做法是区分三种状态:不安全、线程安全且可串行化、线程安全且 MT 安全[5]。可串行化指用同一把互斥量保护全部资源,MT 安全则为每份资源各配一把锁,使互不相同的资源可以并行访问[2]。此外还有不可变、有条件线程安全、线程兼容、线程对立等分类方式,区别在于调用方是否必须自己加外部同步[3]。
原理
线程安全问题的根源在于共享可变状态与不确定的交错执行。在 C 语言里每个线程有自己的栈,但静态变量位于栈外,所有线程同时共享它;若两个线程在同一函数中重叠执行,一个线程可能在另一个线程检查某个静态变量的中途将其改写,这种编译和运行都可能看似正常、却难以复现的错误即竞态条件[2]。以计数自增为例,$i{++}$ 实际包含读取、加一、写回 3 个步骤,两个线程同时执行就可能丢失其中一次更新。
对应的解决思路分为两大类。第一类是避免共享状态:可重入函数把中间状态存放在本次调用独有的局部变量中而不依赖静态或全局变量;线程局部存储为每个线程提供私有副本;不可变对象在构造完成后状态不再改变,只共享只读数据即可天然安全[2]。第二类是针对无法回避的共享进行同步:互斥量把共享数据的访问串行化,保证任一时刻只有一个线程读写;原子操作依赖不可被打断的机器指令,使共享数据始终处于有效状态[2]。
同步还涉及可见性而不只是原子性。声明为 volatile 的变量在 Java 中可保证一个线程的写入对后续读取该变量的线程可见,并能阻止相关指令重排,但它不保证原子性,因而不能替代加锁,只适合写入不依赖当前值的场合[6]。比较并交换(compare-and-swap)一类指令则构成了许多无锁数据结构的基础,其语义是比较内存位置当前值与期望值,相同才写入新值。
flowchart LR
A[多个线程] --> B{是否共享可变状态}
B -- 否 --> C[局部变量 / 线程局部存储 / 不可变对象]
B -- 是 --> D{采用何种同步}
D -- 互斥 --> E[锁保护临界区]
D -- 原子 --> F[CAS 等原子操作]
E --> G[行为与规格一致]
F --> G
发展历程
并发编程的实践基础在 20 世纪 60 年代中期形成。信号量等同步记号在此期间出现,随后管程、条件临界区、同步消息通信等概念相继被提出,为共享数据的受控访问提供了语言层面的构造。线程本身早在 1967 年就以任务之名出现在 OS/360 的多道程序设计系统中,而多线程在应用程序中普及则要等到 2000 年代初多核处理器兴起之后[7]。
原子操作与锁的工程化在 2000 年代前后进入主流语言库。2004 年发布的 Java 5 在语言中加入了 java.util.concurrent 包,提供了显式锁、原子变量、并发容器与线程池等构件,此前该语言只提供 synchronized 这一种一致性原语,在多核机器上容易遇到扩展性瓶颈[8][9]。此后 JVM 又对内置锁做了偏向锁、轻量级锁、自适应自旋等优化,降低了低竞争场景下的开销[10]。
另一条演进线索是把线程调度从操作系统下沉到运行时。Java 21 引入的虚拟线程由虚拟机管理、不直接一一对应操作系统线程,阻塞时可以让出载体线程,从而支持更大规模的并发;但其可伸缩性改善针对阻塞与 I/O 场景,并不消除共享可变数据带来的竞争问题[9][11]。
应用
线程安全的要求广泛出现在程序库的接口约定中。软件库可以只给出部分保证,例如允许并发读是安全的而并发写不是;使用该库的程序是否安全,取决于是否按这些保证来调用[2]。C 标准库与多数 Unix 系统调用本身是线程安全的,少数例外如 asctime、ctime、localtime、rand 因返回静态变量或保存跨调用状态而有对应的 _r 版本[12]。
在 Java 等语言中,这一概念落到具体构件上:synchronized 方法或代码块用于保护复合操作,原子类用于无锁的计数与状态更新,并发集合用于替代在并发访问下会出错的普通集合,阻塞队列则作为生产者与消费者之间的线程安全缓冲[13][14]。操作系统内核与虚拟机自身也用这些机制实现线程调度、垃圾回收和锁。
局限
线程安全并不等于可重入。一个用互斥量保护自增的函数在多线程下是安全的,但若在信号处理程序中被再次调用,第二次调用会阻塞在已被自己持有的锁上且无法释放,从而永久停住;这正是因为 pthread 的普通互斥量不可重入[1]。反过来说,可重入性是比线程安全更强的性质,二者不能互相推出[12]。
同步本身会引入新的风险。互斥量的使用若不周全会带来死锁、活锁与资源饥饿;线程安全保证通常包含防止死锁的设计措施,但在涉及回调与分层结构被破坏时,库层面无法给出完全的无死锁承诺[2]。持有锁的线程若被延迟执行,所有需要该锁的线程都会停摆,优先级反转也可能随之出现。
此外,加锁有性能代价,竞争激烈时上下文切换与调度开销会抵消并发收益[15]。即便容器方法本身已同步,迭代器仍可能不安全,先检查再取值的复合序列也仍可能产生竞态,调用方必须自行加锁或使用更粗粒度的并发构件[16]。单一互斥保护全部资源的做法还会把并发退化为串行,形成性能瓶颈[13]。并发缺陷难以复现和测试,因此论证并发程序的正确性通常需要显式记录其安全论证[17]。
参见
参考资料
- 线程安全 [引用日期2026-10-29]
- Thread-safe [引用日期2026-10-29]
- [科普中国]-线程安全 [引用日期2026-10-29]
- linux什么是线程安全 [引用日期2026-10-29]
- 线程安全 [引用日期2026-10-29]
- viewpagesrc [引用日期2026-10-29]
- Thread (computing) [引用日期2026-10-29]
- refactoring-java-programs-using-concurrent-libraries [引用日期2026-10-29]
- Java 并发编程的演进——从 synchronized 到 Loom 的虚拟线程 [引用日期2026-10-29]
- Synchronized在JDK中的版本变化 [引用日期2026-10-29]
- Java并发编程的哲学:从synchronized到虚拟线程的思维演进 [引用日期2026-10-29]
- Thread-Level Parallelism [引用日期2026-10-29]
- Techniques for concurrency [引用日期2026-10-29]
- JAVA并发编程 [引用日期2026-10-29]
- Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制 [引用日期2026-10-29]
- Reading 18: Thread Safety - Software in 6.005 [引用日期2026-10-29]
- Reading 20: Thread Safety [引用日期2026-10-29]