java并发编程实战听课笔记(2) 可见性、原子性和有序性问题:并发编程Bug的源头
为何会出现并发问题
根源
为了加速程序执行速度、提高资源利用率,有了各种技术,而这些技术也带来了不同的副作用:
- CPU有缓存,缓存导致可见性问题
- 操作系统有进程、线程,分时复用CPU,线程切换带来了原子性问题
- 编译优化,带来有序性问题
CPU缓存带来的可见性问题
如果是单核CPU时代,CPU有缓存,没什么影响,因为只有一个CPU、这个CPU里的缓存只能按顺序访问,那么程序A更新缓存值后,程序B再执行,必然能看到这个结果。
但进入多核时代,每个核的CPU都有自己的缓存,那么不同线程运行在不同CPU上,更新后的数据就不一定保证对运行在其他核的程序可见。
线程切换带来的原子性问题
操作系统分时复用CPU,做任务切换时,切换发生在CPU指令级别上,不是高级语言的一条语句上。典型例子:java中的count++;
这条语句,实际上在CPU上运行时,大致会分为3条CPU指令:
将count的值从内存写入到CPU寄存器
CPU寄存器+1,得到结果
将结果写入到内存中
在这3条指令执行时,如果没有并发控制、就有可能被打断,进而导致程序出错。
编译优化带来的有序性问题
典型案例:双重检查创建单例对象
示例:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
问题:instance = new Singleton();
这条命令实际上是分3步执行,编译优化可能导致此处instance已被赋值、但赋值的内存地址所指对象是null,若此时发生多线程并发访问,就可能出错。
总结
在采用一项技术的同时,一定要清楚它带来的问题是什么,以及如何规避。
参考文章
极客时间版权所有: https://time.geekbang.org/column/article/83682