线程安全底层原理解析

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();

你可能感兴趣的:(线程安全底层原理解析)