并发编程Bug的源头
我们都知道,CPU、内存和I/O设备有着明显的速度差异,而为了合理利用CPU的高性能,平衡这三者之间的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
- CPU增加了缓存,以均衡与内存的速度差异;
- 操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异;
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。
这些优化就是并发程序诡异问题的根源所在,总的来说就是缓存导致的可见性问题、线程切换带来的原子性问题、以及编译优化带来的有序性问题。
1. 可见性问题
CPU缓存带来了可见性问题。为什么这么说?我们首先来看下CPU缓存的工作原理:
CPU要读取一个数据时,首先会从缓存中查找,如果找到就立即读取并送给CPU处理;如果没有找到,就用相对慢的速度从内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。
在多核时代,每个CPU都有自己的缓存,缓存之间是独立的。多个线程在不同的CPU上执行时,当线程A第一次读取内存中变量V时,就会将V调入缓存中;如果在线程A执行的过程中,线程B修改了V的值;之后A再次读取V,读到的仍是上次调入到缓存中的值,这就产生了可见性问题。
下面我们通过一段代码来分析一下可见性问题。
VisibilityTest.java
public class VisibilityTest {
public static void main(String[] args) throws Exception {
VisibilityThread v = new VisibilityThread();
v.start();
Thread.sleep(1000);
System.out.println("即将置stop值为true");
v.stopIt();
Thread.sleep(1000);
System.out.println("finish main");
System.out.println("main中通过getStop获取的stop值:" + v.getStop());
}
}
VisibilityThread.java
public class VisibilityThread extends Thread{
private boolean stop = false;
@Override
public void