并发编程Bug的三种源头
-
源头之一:缓存导致的可见性问题
一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性
在多核CPU上,线程A使用线程 A 操作的是CPU-1 上的缓存,而线程B操作的是CPU-2上的缓存,这是A对V的操作,就不具备可见性
测试代码:
public class Test {
private long count = 0;
private void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
public static long calc() {
final Test test = new Test();
// 创建两个线程,执行 add() 操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
// 启动两个线程
th1.start();
th2.start();
// 等待两个线程执行结束
th1.join();
th2.join();
return count;
}
}
- 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
- 指令 2:之后,在寄存器中执行 +1 操作;
- 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
但操作系统的线程切换可以发生在任一条指令执行完,
编译器为了优化,有时会调整指令的顺序:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
- 上面代码中的getInstance方法,分为三个指令,分配一块内存 M;在内存 M 上初始化 Singleton 对象;然后 M 的地址赋值给 instance 变量。但是实际上优化后的执行路径却是这样的:分配一块内存 M;将 M 的地址赋值给 instance 变量;最后在内存 M 上初始化 Singleton 对象。如果线程A先执行getInstance,当执行完指令2 后发生线程切换,切换到线程B,这时线B判断instance不为null,马上返回了instance实例,但实际上此时的instance并未初始化。
cc- -极客时间:java并发编程学习总结