Part 1
你现在所处的位置
Part 2
高并发编程一直是 Java 领域的高阶内容,有时候 bug 诡异的出现,又诡异的消失,很难重现,很难追踪,让人抓狂。
多线程问题的直接原因想必大家都知道,就是虚拟机 主内存 和 线程工作内存的交互引起的。
首先一个线程是无法看到另一个线程的工作内存的,其次所有线程共享的变量都在主内存中,当线程需要操作某些变量时,不能直接读写主内存,而是要经过如下的步骤:
然后后续就是 synchronize、volatile、wait、notify、Java 锁 相关的知识。
但今天我们换个角度,尝试从计算机硬件的角度来谈谈这个问题。
Part 3
从第一台计算出现,到今天我们在使用的多核高速计算机,CPU、内存、I/O 设备一直在迭代改进,但即时这三者的速度优化到极致,他们的矛盾始终存在:速度差异。CPU 和 内存的速度,可以形象的理解为,CPU 一天,内存一年;内存 和 I/O 设备的速度,可以形象的理解为,内存一天,I/O 设备 10 年。
为了平衡这种差异,计算机体系结构、操作系统、编译程序都做出了相应的贡献:
那么有利必有弊,万物都在平衡中发展,天下没有免费的午餐,很多并发编程 bug 的根源就在这里:
源头之一 :缓存导致的可见性问题
在单核时代,所有的线程都是在一颗 CPU 上运行,CPU 缓存和内存的一致性很容易解决,因为所有线程都是在操作一个 CPU,一个线程对缓存的写,对另一个线程一定是可见的。
一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
但在多核时代,问题就没那么简单了,每颗 CPU 都有自己的缓存,当多个线程在 不同 CPU 上运行时,这些线程操作的是不同的 CPU 缓存,比如下图
线程 A 和 线程 B 操作的是不同 CPU 的缓存,这个时候,线程 A 对变量 V 的操作,就对 线程 B 不可见了。
源头之二 :线程切换带来的原子性问题
由于 I/O 太慢,早期的操作系统就发明了多进程,即时在单核 CPU 上我们也能一边看电影,一边玩游戏。这就是多进程的功劳。
这其中的原理是,操作系统把各个进程的操作分成多个“时间片”,每隔一小段时间,比如50微秒,让一个进程执行一小段时间,过了50微秒,操作系统就让另一个进程继续执行一段时间。
现代操作系统是基于多线程的,因为线程共享一个内存空间,所以切换线程的成本比切换进程的成本低的多。
我们的 Java 虚拟机的任务切换,自然也是基于多线程的。但可能你想不到,任务切换竟然也是 高并发 bug 的源头。
比如 高级语言的 count += 1 这一条指令,我们很容易就会误认为,是一次性执行完的,但其实是三条 CPU 指令:
操作系统做任务切换,可能是发生在任何一条 CPU 指令执行完,注意这里是 CPU 指令而不是一条语句。假设现在有两个线程,他们按照下面的顺序执行,最终的结果是1,而不是2:
我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。
源头之三:编译优化带来的有序性问题
有序性指的是,程序要按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,如,“c1 = 1;c2 = 2”,编译器可能会优化为 “c2 = 2;c1 = 1”,但不影响最终结果。
但有时候,编译器和解释器的优化可能会触发意想不到的 bug,比如最经典的创建单例对象,如下:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
假设两个线程同时调用 getInstance 方法,他们会同时发现 instance == null,于是尝试加锁,虚拟机可以保证只有一个线程(A)加锁成功,另一个线程(B)处于等待状态。线程 A 会创建一个 instance 实例,释放锁,然后 B 获得锁,线程 B 发现 install != null 于是返回。
这一切看上去都无懈可击,非常完美,但问题可能会出现在我们意想不到的地方。出在 new 操作上,我们以为的 new 可能是:
但实际上优化后是这样的:
会导致线程 A 在执行 new 的第二条指令时,发生了线程切换,线程 B 发现 instance 不为 null ,于是直接返回,但这个时候,instance 变量是还未初始化的,于是返回了 null ,可能会触发程序的空指针异常。
Part 4
只要能深刻理解 可见性、原子性、有序性在并发场景下的原理,再联系Java 高并发编程的相关的知识点,理解 Java 为什么会这么设计,解决了什么问题,那么一些 bug 就会逐个击破了。