并发编程幕后
为了合理利用 CPU 的高性能,平衡 CPU、内存、IO 设备 之间的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献主要表现为
- CPU 增加了缓存,以均衡与内存的速度差异
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 IO 设备的速度差异
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用
现在我们几乎所有的程序都享受着这些成果,但天下没有免费的午餐,并发程序很多诡异问题的根源也在这里。
1. 缓存导致可见性问题
可见性:一个线程对共享变量进行修改,另一个线程能够立刻看到。
2. 线程切换带来了原子性问题
原子性:一个或多个操作在 CPU 执行的过程中不被中断
CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。因此,很多时候我们需要在高级语言层面保证操作的原子性。
3. 编译优化带来的有序性的问题
编译器为了优化性能,有时候会改变程序中语句的先后顺序。
// Single Instance Mode
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
理想中的执行顺序:
- 分配一块内存 M;
- 在内存 M 上初始化 Singleton 对象;
- 然后 M 的地址赋值给 instance 变量。
实际上优化后的执行顺序:
- 分配一块内存 M;
- 将 M 的地址赋值给 instance 变量;(如果此时进入了判断 instance 是否为空的条件,得到的结果是不为空的,立刻返回一个没有初始化的 instance,就 GG 了)
- 最后在内存 M 上初始化 Singleton 对象。
个人主页: blog.wumalife.com/