Java并发编程-可见性、原子性、有序性问题引入

版权声明:转载请注明文章来源 https://blog.csdn.net/baofeidyz/article/details/90113051

这篇文章属于读书笔记,学习极客时间Java并发编程实战课程时写下的,部分内容来源于课程

可见性

由于存储的成本和速度问题,我们的计算机采用了多级存储。CPU集成的三级缓存,主内存以及我们常用的硬盘存储。

我们的应用程序从硬盘存储加载到主内存中,当我们的CPU去执行指令运算的时候,会把需要运算的代码块加载到CPU集成的缓存中。每一个CPU都有自己的缓存,所以当我们使用多线程并发编程时,就会出现可见性问题。

如图所示:
极客时间-java并发编程-可见性问题
当我们的使用多线程计算count++的时候,假如现在变量count的值为1。
线程A将变量count加载到CPU-1的缓存中,同时线程B也将变量count加载到CPU-2的缓存中。
他们同时计算count++,然后再将变量count写入(同步)到主内存中,最后程序输出变量count的值为2,但是正确的两次count++应该是3

这就是可见性问题

原子性

上文阐述的是因为使用多个CPU完成多线程运算,实际上单个CPU也是可以完成多线程运算。
单个CPU是使用时间片、多线程分时复用实现的。

我们的应用程序在使用分时复用时,多个线程会同时操作一个内存区域,此时就会造成原子性问题。

如图所示:
极客时间-java并发编程-原子性问题

还是当我们使用多线程计算count++的时候,实际上转换成CPU指令会被拆解成三步,如下所示:

  1. 指令1:首先,需要把变量count从内存加载到 CPU的寄存器
  2. 指令2:之后,在寄存器中执行+1操作
  3. 指令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();

我们认为的顺序应该是

  1. 分配一块内存M
  2. 在内存中初始化Object对象
  3. 然后M的地址赋值给obj变量。

但实际优化以后的执行路径却是这样的:

  1. 分配一块内存M
  2. 将M的地址赋值给obj变量;
  3. 最后在内存中M中初始化Object对象。

也就是说初始化对象的操作可能是在最后完成的,这样会导致什么问题呢?

极客时间-java并发编程-有序性问题

图片是直接引用的,其中的instace等同于objSingleton等同于Object

在双重校验创建单例对象中,如果线程A完成了执行路径中的第二步,但没有完成执行路径中的第三步。此时线程B进入双重校验时,变量obj不为空,此时则会直接返回。当程序访问变量obj的成员方法或者是成员变量时,就会出现NPE。

这就是有序性问题

没有更多推荐了,返回首页