冯诺依曼体系将计算机分为 CPU、运算器、存储器、输入设备、输出设备,奠定了经典计算机的发展
尽管 CPU、内存、IO 设备不断向更快的方向进步,但三者的速度差异一直是核心矛盾
为了缓解这一核心矛盾,计算机、操作系统、编译程序都做了相应的优化
CPU 缓存技术、多线程技术、指令优化技术提高了程序的执行效率,但也使并发程序运行的问题暴露无遗
程序变量存储在内存中,CPU 读写变量时,会操作缓存中的变量副本,以提高运行效率
注意:对于何时把数据从缓存写到内存,没有固定的时间
在单核 CPU 机器上,线程操作同一个内核的缓存,多个线程看见的数据是统一的
在多核 CPU 机器上,每一个核心都有独立的缓存,而线程被调度到不同的核心执行
对同一变量的操作,不同缓存中的变量副本在线程之间不立即可见,导致多线程对共享变量的操作结果错误
假若不考虑可见性问题,对于共享变量的非原子操作,也会引发错误的操作结果
CPU 只能保证每一条指令是原子执行的,但每一条程序语句是不一定的
例如i += 1
,完成这条语句执行需要 3 条指令
指令 1:将变量 i 加入 CPU 寄存器
指令 2:寄存器执行 +1 操作
指令 3:将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)
操作系统进行多线程调度时,可能在任意指令执行完毕时进行线程切换
非原子的程序语句,在语句没有执行完毕就切换线程,导致共享变量被其他线程覆盖
编译器和 CPU 为了优化性能,会改变指令的执行顺序,在并发情况下可能会印象影响程序的运行结果
一条语句可能是多条指令,这些指令的执行顺序可能会被重排
例如双检锁实现单例模式
public class SafeLazyMan {
private static SafeLazyMan man;
public static SafeLazyMan instance() {
if (man == null) { // 第一次检查
synchronized (SafeLazyMan.class) {
if (man == null) { // 第二次检查
man = new SafeLazyMan();
}
}
}
return man;
}
}
为了防止有多个线程完成第一次检查,多次创建对象,增加获取锁之后的第二次检查,保证对象不被重复的创建,但是 new 对象操作分为分配内存、初始化对象、引用赋值三个指令
指令重排后先对引用赋值,再初始化对象,会导致其他线程第一次检查时虽然不为 null 了,但其实没有对象,引发空指针问题
编译器和 CPU 对汇编指令的执行顺序的进行优化,可以缩短执行消耗的时钟周期,但可能会导致不符合并发程序运行的预期结果