# 背景

CPU, 内存, IO设备是计算机三个重要的组成, 性能好坏也由它们协调所致.

为了平衡三者速度的差异:

  1. CPU上增加了缓存, 以均衡内存.
  2. 操作系统增加了进程/线程, 以均衡CPU和IO
  3. 编译器优化指令执行次序, 使缓存能更合理的利用

# 可见性

释义: 一个线程对共享变量的修改, 另一个线程能够马上看见.

在单核情况下, 所有线程都是操作同一个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变量, 会禁止编译器指令重排.

修改于: 8/11/2022, 3:17:56 PM