java并发编程实战《一》可见性、原子性和有序性

可见性、原子性和有序性问题:并发编程Bug的源头

核心矛盾:CPU、IO、内存三者之间的速度差异。

 

为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

1.CPU 增加了缓存,以均衡与内存的速度差异;

2.操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;

3.编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

 

缓存导致的可见性问题

图片来自极客时间java并发编程实战

 

一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

 

有必要提一下的是,一个线程如何修改共享变量?

线程有自己的工作内存,可概称为栈(出自《深入理解java虚拟机》第二版,12.3.1 主内存与工作内存),而你的数据是存在主存中的,线程修改数据时先从主存中拷贝你的数据复制到自己的内存中修改,然后写回主存。

如果两个线程同时做上述操作,就引发了可见性问题。

 

线程切换带来的原子性问题

 

所谓的原子性问题即原子操作,即不会被线程调度机制打断的操作。

比如这个样子

图片来自极客时间java并发编程实战

在一个时间片内,如果一个进程进行一个 IO 操作,例如读个文件,这个时候该进程可以把自己标记为“休眠状态”并出让 CPU 的使用权,待文件读进内存,操作系统会把这个休眠的进程唤醒,唤醒后的进程就有机会重新获得 CPU 的使用权了。

这里的进程在等待 IO 时之所以会释放 CPU 使用权,是为了让 CPU 在这段等待时间里可以做别的事情,这样一来 CPU 的使用率就上来了;此外,如果这时有另外一个进程也读文件,读文件的操作就会排队,磁盘驱动在完成一个进程的读操作后,发现有排队的任务,就会立即启动下一个读操作,这样 IO 的使用率也上来了。

是不是很简单的逻辑?但是,虽然看似简单,支持多进程分时复用在操作系统的发展史上却具有里程碑意义,Unix 就是因为解决了这个问题而名噪天下的。

图片来自极客时间java并发编程实战

 

编译优化带来的有序性问题

例如java双重检查锁

 1 public class Singleton {
 2   static Singleton instance;
 3   static Singleton getInstance(){
 4     if (instance == null) {
 5       synchronized(Singleton.class) {
 6         if (instance == null)
 7           instance = new Singleton();
 8         }
 9     }
10     return instance;
11   }
12 }

假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null ,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。

这看上去一切都很完美,无懈可击,但实际上这个 getInstance() 方法并不完美。问题出在哪里呢?

出在 new 操作上,我们以为的 new 操作应该是:

1.分配一块内存 M;

2.在内存 M 上初始化 Singleton 对象;

3.然后 M 的地址赋值给 instance 变量。

 

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

1.分配一块内存 M;

2.将 M 的地址赋值给 instance 变量;

3.最后在内存 M 上初始化 Singleton 对象。

 

优化后会导致什么问题呢?我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

图片来自极客时间java并发编程实战

 

课后思考

常听人说,在 32 位的机器上对 long 型变量进行加减操作存在并发隐患,到底是不是这样呢?

long类型64位,所以在32位的机器上,对long类型的数据操作通常需要多条指令组合出来,无法保证原子性。

 

摘自极客时间王宝令老师的课程

 

posted @ 2020-05-19 07:57  夜旦  阅读(163)  评论(0编辑  收藏  举报