多线程安全的三大条件
总结来说,多线程安全主要要满足 三大条件:
原子性(Atomicity)
操作不可分割,要么全部成功,要么全部失败。
1 | int x = 0; |
可见性 (Visibility)
一个线程对共享变量的修改,另一个线程能够立即看到。
CPU 会做缓存[1]和指令重排(跟有序性也有很大关系),导致线程 A 改了变量,线程 B 可能“看不到”。
需要内存屏障(memory barrier)[2]、volatile
或锁来保证。
1 | volatile bool flag = false; |
有序性 (Ordering)
程序执行顺序符合预期。
编译器,JIT和CPU,可能会重排指令,导致代码执行顺序和书写顺序不一致。
内存屏障、锁等可以限制重排。
1 | class Singleton { |
这个时候可以给 instance 添加 volatile
,或者是用 Lazy<T>
(线程安全的初始化对象)
总结
在多线程编程里,原子性(Atomicity)、有序性(Ordering)、可见性(Visibility) 经常是相辅相成的——它们不是独立孤立的,而是互相依赖来保证线程安全。
脚注
[1] 现代的 cpu 大多有多级缓存,比如 L1,L2和L3,RAM,这么做是为了减少访存的时间。
缓存层级 | 特点 |
---|---|
L1 Cache | CPU 核心私有,最小、最快 |
L2 Cache | CPU 核心私有,稍大,速度略慢 |
L3 Cache | 共享缓存,多核心共享,速度比 L1/L2 慢,但比内存快 |
主存 (RAM) | 所有核心共享,访问最慢 |
一般在cpu核心读操作时先从最近的缓存找,如果能命中缓存就直接读,不会再访问其他缓存。写操作时先写到缓存再延迟写回主存(根据cpu决定,通常也会写入 L2/L3,写主存很慢,所以这个操作是延迟的)。
[2] 内存屏障是编译器和CPU提供的用来限制指令的重排序和控制缓存可见性的指令/机制。
编译器为了性能可能会做指令重排,寄存器缓存。
为了避免这种优化导致问题,编译器会提供屏障语义:
比如编译器会在volatile
变量前后插入内存屏障,保证可见和有序。
比如 lock
,编译器会在周围插入 full fence(禁止编译器和cpu的重排序,同时保证读写可见性)。
CPU同样可能会为了性能去乱序执行,写缓冲区延迟写回等。
为了避免这种优化导致问题,cpu有专门的指令:
x86 有 mfence
、lfence
、sfence
ARM 有 dmb
、dsb
这些指令就是内存屏障。