# 背景
CPU, 内存, IO设备是计算机三个重要的组成, 性能好坏也由它们协调所致.
为了平衡三者速度的差异:
- CPU上增加了缓存, 以均衡内存.
- 操作系统增加了进程/线程, 以均衡CPU和IO
- 编译器优化指令执行次序, 使缓存能更合理的利用
# 可见性
释义: 一个线程对共享变量的修改, 另一个线程能够马上看见.
在单核情况下, 所有线程都是操作同一个CPU缓存, 内存中数据只需要与CPU缓存保持一致就 不存在
可见性问题. 而多核情况下, 每颗CPU都有自己独立的缓存, 此时内存中的数据要与CPU缓存保持一致性就有困难了.
# 原子性
一个或多个操作在CPU执行的过程中不会被中断的特性称为原子性.
CPU能保证的原子操作是CPU指令级别的, 而不是高级语言的最小操作单元.
# 并发场景
下面是count+=1的场景, 两个线程同时执行.
| Thread A | Thread B |
|---|---|
| count=0加载到线程寄存器中 | |
| 线程切换---> | count=0加载到线程寄存器中 |
| count+1=1 | |
| count=1写入内存 | |
| count+1=1 | <---线程切换 |
| count=1写入内存 |
# 有序性
编译器为了优化性能, 有时候会改变程序中语句的先后顺序.
大多数顺序的优化都不会影响程序执行的结果, 但偶尔会出现意想不到的Bug.
# 场景重现
下面这个例子是
双重检查锁实现单例模式, 在编译器优化的代码的过程中, 可能会出现指令重排.但是我没有重现出来- -!
public class Singleton {
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){
if (instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
场景说明
两个线程A,B, A先进入synchronized同步块, 但是在new对象的时候发生了指令重排. new本身会发生三个步骤, 1分配内存空间, 2初始化Singleton对象, 3将内存空间地址赋值给instance变量. 此处指令重排是将步骤2和3顺序改成了先执行3后执行2, 如果线程切换发生在执行3之后执行2之前, 此时线程B获得时间片, 判断第一个if, instance变量是!=null的, 所以直接return instance, 但instance变量中的地址指向的内存空间实际上是没有被初始化的, 也就是线程B可能接下来执行的逻辑会出现NPE.
解决方案
使用volatile修饰instance变量, 会禁止编译器指令重排.