线程安全底层原理解析

作者: 降龙_伏虎 | 来源:发表于2019-09-26 14:20 被阅读0次

    1 什么是可见性?

    • 通过 volatile 修饰的变量被a线程修改b线程能立即读取到修改后的值,不会出现'脏读'

    2 可见性原理

    • volatile修饰后hsdis多了个Lock汇编指令,Lock汇编指令是一种控制指令,作用是在多线程环境中,可以基于总线锁缓存锁的机制来达到共享变量在线程间的可见性

    3 硬件层面

    • CPU>内存>IO 硬件方面存在很大的处理速度的差异,木桶原理最---最短板决定整体性能
    • 所以硬件方面的性能优化要从两方面着手:
      ①提高短板(基本不可实现)
      ②最大化利用【性能过剩组件(CPU)】

    3.1 最大化利用CPU方法

    image.png
    • CPU增加高速缓存,cpu绝大多数的业务处理中都会依赖内存或者IO进行运算或数据存储
    • CPU告诉缓存通过降低内存/IO读取频率来实现提高整体处理性能
    • CPU高速缓存分为:L1>L2>L3三种,性能依次下降
    image.png
    • L1d:L1数据缓存
    • L1i:L1指令缓存
    • CPU高速缓存提高了CPU处理过程中频繁与主内存交互的性能
    • CPU高速缓存也带来了缓存(数据)一致性的问题

    3.2 缓存(数据)一致性解决方案:

    • 总线锁
      通过在总线添加锁的方式来保证缓存(数据)一致性,当cup0通过总线操作数据时,其它cpu1将无法获取总线的使用权限,对性能影响很大
    • 缓存锁
      相对于总线锁缓存锁的范围更加精确,降低看控制粒度,通过缓存一致性协议实现
    • 缓存一致性协议MESI
      不同的CPU架构里缓存一致性协议有着各自不同的实现方式,X86架构中是基于MESI协议
    image.png image.png
    image.png

    M>Modified 修改状态
    E>Exclusive 独享状态
    S>Shared 共享状态:表示数据可被多个缓存对象进行缓存,且数据值与主内存一致
    I>Invlid 失效状态
    失效状态缓存不可被使用,将从主内存中进行读取

    3.3 MESI的局限性

    • 当某个CPU修改缓存中的数据时,首先通知其cup缓存中的相同数据,其它相同缓存置为失效
      其它CPU缓存失效完成后再通知要修改的CPU,该过程中CUP处于阻塞中,浪费了CPU性能


      image.png

    3.4 EMSI 改进

    • 为了减少缓存被修改过程中的阻塞时长,通知修改时采用异步操作,不进行阻塞
      将修改请求缓存到storebuffer中


      image.png
    • storebuffer带来的问题
    value =3;
    void cup0{
      value = 10;// 通过storebuffer异步通知其他cpu缓存,将缓存value变为I:失效状态
      isFinish = true; //E 独占状态
    }
    
    void cup1{
        //由于cup0中storebuffer是异步操作
        //所以理论上村 isFinish=true 而 value=3 这种情况
        if(isFinish){//true 
            assert value == 10;//false
        }
    }
    

    storebuffer可能会导致cup的乱序执行既"指令重排序",重排序将带来可见性问题

    • 硬件层面的优化,总是会带来其他问题,无法真正解决可见性问题,所以cpu层面提供指令--内存屏障供软件方面调用

    3.5 内存屏障

    value =3;
    void cup0{
      value = 10;// 通过storebuffer异步通知其他cpu缓存,将缓存value变为I:失效状态
      加入内存屏障
      isFinish = true; //E 独占状态
    }
    
    void cup1{
        //由于cup0中storebuffer是异步操作
        //所以理论上村 isFinish=true 而 value=3 这种情况
        if(isFinish){//true 
            读取内存屏障//由于cup0在  'sFinish = true; //E 独占状态' 前加入内存屏障
                                  //所以下面代码中value值将,直接从主内存中进行获取  
            assert value == 10;//false
        }
    }
    
    • cup层面提供了3中内存屏障
      读屏障 store barrier
      写屏障 load barrier
      全屏障 full barrier

    • X86架构中volatile关键字的实现依赖:volatile--->Lock指令(缓存锁)--->内存屏障

    • 内存屏障/指令重排序 等和平台一级硬件有关,不同硬件是不同的实现.java是跨平台语言,不需要在业务点中考虑硬件的差异性的是依托于JMM内存模型的存在

    4 JMM虚拟内存模型

    image.png
    • 语言基本的抽象内存模型,本与cpu内存模型相类似
    • 线程通过操作工作内存来修改数据,工作内存负责和主内存进行通信和数据同步
    • JMM虚拟内存模型为作为一种标准,不同的硬件设备有着各自的实现(指令).通过JMM业务代码开发人员不需要关系硬件差异化,从而实现语言的跨平台

    4.1 重排序

    • 代码重排序顺序:源代码->编译器重排序->CPU层面重排序(指令级、内存)->最终执行的指令
    • 通过重排序可以提高代码效率,但不是所用情况都会进行重排序,是否重排序取决于【数据依赖规则】
    /**
    * 无数据依赖
    * 1&2行代码间无相互依赖
    * 可进行从排序
    */
    int a = 1;
    int b = 2;
    
    
    /**
    * 部分数据依赖
    *  1&2 行代码间无数据依赖
    *  1&3 行代码间存在数据依赖
    *  2&3 行代码间存在数据依赖
    * 1&2行可进行重排序 1&3 2&3 行不可重排序
    */
    int a=1;
    int b = 2;
    int c = a+b;
    
    • 数据依赖规则:as-if-serial
      无论代码以何种方案进行重排序,对于单个线程执行代码的结果不可变

    • Happens-Befor
      代码A代码的执行结果对于B代码必须是可见,就成为 A Happens-Befor B

    • 那些场景会触发Happens-Before规则?
      ① 【程序的顺序规则】

    /**
    * 单线程调用该方法时,A Happens-Befor B
    **/
    function X(){
     a =1;// A
     b =2;// A
    }
    

    ② 【volatile规则】
    volatile修饰的变量写操作一定对读操作可见,即 "写" Happens-Befor "读"
    ③【传递性规则】
    如果 :A Happens-Befor B & B Happens-Befor C
    那么: A Happens-Befor C
    ④ 【start规则】
    主线程里的start()方法 Happens-Befo 该线程run方法内任意代码

    /**
    *  B Happens-Befor C (start规则)
    *  A Happens-Befor B (顺序规则)
    *  A  Happens-Befor C  (传递性规则)
    **/
    public class A{
      static x=0;
      public static  void main(String []args){
        Thread t1=new Thread(()->{
          //C .....
        });
        x=10;//A
        t1.start();//B
      }
    }
    

    ⑤【Join规则】
    线程run方法内代码 Happens-Befor join()后的代码

    public class Demo {
        
        static  int a = 0;
        /**
         * A Happens-Befor B
        **/
        public static void main(String[] args) throws Exception {
            Thread t1 = new Thread(()->{
                a = 99;//A
            });
            t1.start();
            t1.join();
            System.out.println(a);//B
        }
    }
    

    ⑥ 【synchronized监视器锁规则】
    synchronized 的占用顺序决定线程代码顺序

    public class Demo {
        public  void  xx(){
            synchronized (this){
                //A...
            }
        }
    
        public static void main(String[] args) {
            
            /**
             *t1 线程t1 代码A Happens-Befor 线程t2 代码A
             *
             **/
            Demo demo = new Demo();
            Thread t1 = new Thread(()-> demo.xx());
            Thread t2 = new Thread(()-> demo.xx());
            t1.start();
            t1.join();//保证t1先于t2
            t2.start();
        }
    }
    
    • 无数据依赖情况下禁止重排序
      当代码间不存在数据依赖,但在多线程调用的场景下可能会导致执行结果错误,此时需要人工干预重排序---JMM内存屏障
    value =3;
    void cup0{
      value = 10;// 通过storebuffer异步通知其他cpu缓存,将缓存value变为I:失效状态
      isFinish = true; //E 独占状态
    }
    
    void cup1{
        if(isFinish){//true 
            assert value == 10;//false
        }
    }
    
    • JMM内存屏障:编译器级别内存屏障、CPU级别内存屏障

    4.2 JMM解决有序性、可见性方案

    • volatile
      可解决可见性。通过内存屏障实现
    • synchronized
      可解决可见性、有序性、原子性。通过对线程阻塞实现单线程调用来实现
    • final
      遍历不可变,避免了可见性、原子性等问题
    • happens-before

    5 线程的顺序执行

    • 使用join
      阻塞主线程,直到调用join()方法的线程执行完毕;或者说调用join()线程的执行结果对主线程可见,底层通过wait/notify实现
    /**
    * 只有添加join后线程才会123依次执行
    **/
    Thread t1 = new Thread(()->{
        //doSomething1
    });
    Thread t2 = new Thread(()->{
        //doSomething2
    });
    Thread t3 = new Thread(()->{
        //doSomething3
    });
    t1.start();
    t1.join();
    t2.start();
    t2.join();
    t3.start();
    

    相关文章

      网友评论

        本文标题:线程安全底层原理解析

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