这篇文章属于读书笔记,学习极客时间Java并发编程实战课程时写下的,部分内容来源于课程
可见性
由于存储的成本和速度问题,我们的计算机采用了多级存储。CPU集成的三级缓存,主内存以及我们常用的硬盘存储。
我们的应用程序从硬盘存储加载到主内存中,当我们的CPU去执行指令运算的时候,会把需要运算的代码块加载到CPU集成的缓存中。每一个CPU都有自己的缓存,所以当我们使用多线程并发编程时,就会出现可见性问题。
如图所示:
当我们的使用多线程计算count++
的时候,假如现在变量count
的值为1。
线程A将变量count
加载到CPU-1的缓存中,同时线程B也将变量count
加载到CPU-2的缓存中。
他们同时计算count++
,然后再将变量count
写入(同步)到主内存中,最后程序输出变量count
的值为2,但是正确的两次count++
应该是3
这就是可见性问题
原子性
上文阐述的是因为使用多个CPU完成多线程运算,实际上单个CPU也是可以完成多线程运算。
单个CPU是使用时间片、多线程分时复用实现的。
我们的应用程序在使用分时复用时,多个线程会同时操作一个内存区域,此时就会造成原子性问题。
如图所示:
还是当我们使用多线程计算count++
的时候,实际上转换成CPU指令会被拆解成三步,如下所示:
- 指令1:首先,需要把变量count从内存加载到 CPU的寄存器
- 指令2:之后,在寄存器中执行+1操作
- 指令3: 最后,将结果写入到主内存(缓存机制可能会导致此处写入到的是缓存而不是内存)
线程A在执行指令1之后,CPU时间片切换,执行线程B,此时线程B直接执行完三个指令,并写入到内存(或缓存)中。此时缓存中已经是变量count
已经是1了,但是由于线程A已经完成了指令1,在线程A的指令2中,变量count
依然是0,所以线程A完成指令2、指令3以后,写入内存(或缓存)中的变量count
依然是1。而我们的期望值应该是2。
这就是原子性问题:CPU指令的原子操作
有序性
有序性问题主要是因为编译优化,我们写的代码逻辑可能与最后编译后代码逻辑不同。
比如
Object obj = new Object();
我们认为的顺序应该是
- 分配一块内存
M
- 在内存中初始化
Object
对象 - 然后M的地址赋值给
obj
变量。
但实际优化以后的执行路径却是这样的:
- 分配一块内存
M
; - 将M的地址赋值给
obj
变量; - 最后在内存中M中初始化
Object
对象。
也就是说初始化对象的操作可能是在最后完成的,这样会导致什么问题呢?
图片是直接引用的,其中的
instace
等同于obj
,Singleton
等同于Object
在双重校验创建单例对象中,如果线程A完成了执行路径中的第二步,但没有完成执行路径中的第三步。此时线程B进入双重校验时,变量obj
不为空,此时则会直接返回。当程序访问变量obj
的成员方法或者是成员变量时,就会出现NPE。
这就是有序性问题