美文网首页
多线程安全问题:可见性、原子性、有序性

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

作者: 潇湘哥哥 | 来源:发表于2020-11-07 19:02 被阅读0次

    引言


    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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/cllqbktx.html