引言
- CPU缓存与内存产生的一致性问题
- CPU时间片切换产生的原子性问题
- CPU指令编译优化产生的有序性问题
并发程序问题的根源
- CPU、内存、I/O设备三者速度差异一直是 核心矛盾
三者速度差异可形象描述为:天上一天(CPU),地上一年(内存),地上十年(I/O)
根据木桶理论,程序整体性能取决于最慢的操作-读写I/O设备,可见单方面提高CPU性能是无效的
为了合理利用CPU的高性能,平衡三者的速度差异,计算机体系结构、操作系统、编译程序都做了努力:
- CPU增加了缓存,以均衡与内存的速度差异
- 操作系统增加了进程、线程,以及分时复用CPU,进而均衡CPU与I/O设备的速度差异
- 编译程序优化指令执行次序,使得缓存能够得到更加合理的利用
源头之一:缓存导致的可见性问题
- 什么是可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
-
多核时代,每颗 CPU 都有自己的缓存, CPU 缓存与内存的数据一致性就没那么容易解决了
image.png
public class ThreadDemo {
private int count = 0;
public void add10K() {
for (int i = 0; i < 10000; i++) {
count += 1;
}
}
public void calc() throws InterruptedException {
Runnable task = new Runnable() {
@Override
public void run() {
add10K();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count=" + count);
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo demo = new ThreadDemo();
demo.calc();
}
}
源头之二:线程切换带来的原子性问题
-
CPU时间片
image.png
示例:count += 1,至少需要三条 CPU 指令
- 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
- 指令 2:之后,在寄存器中执行 +1 操作;
- 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
带来可能问题

- 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性
CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符
源头之三:编译优化带来的有序性问题
- 有序性指的是程序按照代码的先后顺序执行
示例:利用双重检查创建单例对象
public class Singleton {
private Singleton() {}
// private static volatile Singleton instance;
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
new的理论顺序:
- 分配一块内存M
- 在内存M上初始化Singleton对象
- M的地址赋值给instance变量
经过编译器实际优化后:
- 分配一块内存M
- M的地址赋值给instance变量
- 在内存M上初始化Singleton对象
带来问题:

网友评论