多线程安全问题:可见性、原子性、有序性

引言


  1. CPU缓存与内存产生的一致性问题
  2. CPU时间片切换产生的原子性问题
  3. CPU指令编译优化产生的有序性问题

并发程序问题的根源


  1. CPU、内存、I/O设备三者速度差异一直是 核心矛盾
    三者速度差异可形象描述为:天上一天(CPU),地上一年(内存),地上十年(I/O)
    根据木桶理论,程序整体性能取决于最慢的操作-读写I/O设备,可见单方面提高CPU性能是无效的

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

  • CPU增加了缓存,以均衡与内存的速度差异
  • 操作系统增加了进程、线程,以及分时复用CPU,进而均衡CPU与I/O设备的速度差异
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理的利用

源头之一:缓存导致的可见性问题


  1. 什么是可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
  2. 多核时代,每颗 CPU 都有自己的缓存, CPU 缓存与内存的数据一致性就没那么容易解决了


    image.png
public class ThreadDemo {
    private int count = 0;

    public void add10K() {
        for (int i = 0; i < 10000; i++) {
            count += 1;
        }
    }

    public void calc() throws InterruptedException {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                add10K();
            }
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count=" + count);
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadDemo demo = new ThreadDemo();
        demo.calc();
    }
    
}

源头之二:线程切换带来的原子性问题

  1. CPU时间片


    image.png

示例:count += 1,至少需要三条 CPU 指令

  • 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
  • 指令 2:之后,在寄存器中执行 +1 操作;
  • 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

带来可能问题


image.png
  1. 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性
    CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符

源头之三:编译优化带来的有序性问题

  1. 有序性指的是程序按照代码的先后顺序执行

示例:利用双重检查创建单例对象

public class Singleton {
    private Singleton() {}

   // private static volatile Singleton instance; 
    private static Singleton instance; 
    
    public  static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

new的理论顺序:

  1. 分配一块内存M
  2. 在内存M上初始化Singleton对象
  3. M的地址赋值给instance变量

经过编译器实际优化后:

  1. 分配一块内存M
  2. M的地址赋值给instance变量
  3. 在内存M上初始化Singleton对象

带来问题:


image.png

你可能感兴趣的:(多线程安全问题:可见性、原子性、有序性)