多线程可见性的本质

线程安全性原理

1)volatile
    主线程里面改变的值,无法使得T1线程可见,导致依然退不出来。

    volatile的目的在于:在多个处理器下,保证多个共享变量的可见性。

    可见性: 别的线程修改的值,别的线程无法连读取到最新的值。

    hsdis工具:
        多了一个Lock的汇编指令。 基于总线或者缓存,达到可见性的目的。


2)可见性到底是什么? --》硬件层面了解可见性的本质
     CPU、内存、磁盘IO设备。
     矛盾点: 3者之间的速度差异很大。

     程序中要依赖CPU、还依赖内存,还可能依赖网络IO。 程序的性能取决于短板(木桶原理)。
         cpu 1ms, 内存10ms,IO设备100ms,因此计算需要100ms

     我们需要最大化的利用CPU资源:
         1.CPU增加高速缓存:
             CPU调度线程去计算的时候,要依赖于内存,要读取数据。 2个CPU要基于总线去通信,然后从主内存取数据。 在主内存读取IO的过程中,我们的CPU
                 将会被阻塞,这我们是无法接受的。因此我们引入了CPU的高速缓存。 因此内存和CPU之间增加了高速缓存,我们CPU先从高速缓存中读取数据。

             有3种缓存:L1、L2、 L3。 其中L1 L2是CPU私有的
                 指令缓存
                 数据缓存


                         共享的L3


               L2Cache
             

             L1d    L1i

                 regs    

         解决了速度访问差异,但是引来了新的问题:缓存不一致问题:因此CPU层面
            1.总线锁:
                CPU要从主内存拿数据时,一定会先通过总线,主内存访问的互斥特性。 因此又导致串行。 因此引入了缓存锁(降低了锁的控制粒度)

            2.缓存锁    
                缓存一致性协议(MESI、。。。):
                    缓存行的4种状态:
                        CPU0:
                            cache i = 0;

                    Shared: 被多个CPU缓存了
                    Invalid:有一个缓存已经失效了
                    Modify: Exclusive
                        嗅探协议: 一个CPU改为了1,,那么状态由S变为M,让CPU1的缓存行处于失效状态,使得CPU1箱再次读取I的值,需要去主内存拿,
                            而不是去自己的缓存行去拿,因此自己的缓存行失效了,这时就打到了缓存一致性的效果。

                        缓存处于: MES状态都可以读取,只有I状态的CPU,必须从主内存读取。

                总体:对于MESI,就缓存一致性。 但是肯定还是没有解决缓存可见性问题,不然也就没必要增加volatile关键字了。
                            各个CPU的缓存行需要一致

                什么时候数据同步到主内存呢?


                  主内存i=0
         CPU1:                   CPU2:
             i=0                       i=0
             i++ --》i=1         

         这时,CPU1修改为了i=1,但是还没有同步到主内存


         storebuffer:
             让storebuffer, 是一个异步流程。提高CPU利用率。但是再次带来了可见性的问题--》说明硬件层面无法彻底解决可见性问题--》引出内存屏障。


             value = 3;

             void cpu0(){
                 value=10;        S->M状态。 --》会先写storebuffer--》通知其他cpu,让其他cpu缓存失效(这个是异步的)。 因此晚于isFinish。CPU的乱序性。重排序。
                                                             重排序会导致可见性问题。 因此硬件层面怎么优化,都无法彻底解决可见性问题。
                                                             因此CPU层面提供了内存屏障的指令。程序里面在适当的地方加入内存屏障,从而达到可见性的目的。


                 storeMemoryBarrier();    // 屏障指令                                        
                 isFinish = true;  E状态。 
             }


             void cpu1(){
                 if(isFinish){            // true

                     loadMemoryBarrier();    // 使得直接和主内存交互

                     assert value ==10; // false
                 }
             }

             异步化带来的可见性问题。


             CPU层面提供3种屏障:
                 写屏障: store barrier
                 读屏障:  load barrier  
                 全屏障: full barrier 读+写


                 volatile: lock(缓存锁)-->内存屏障--》可见性


                 内存屏障、重排序 --》和平台以及硬件有关,但是java是一次编译到处运行。--》因此引入了JMM

         2.引入线程、进程的概念。 线程阻塞情况下,让CPU进行上下文切换。

         3.指令的优化: 重排序

3)可见性到底是什么? --》什么是JMM

    可见性问题的根本原因: 高速缓存和重排序。

    禁止缓存和禁止重排序。

    JMM最核心的价值:解决 有序性和可见性 的问题。

    语言级别的抽象内存模型:
        使用内存屏障去禁止重排序。


                主内存
    工作内存                工作内存
    线程A                    线程B

        线程无法直接读取主内存,而是要先读取工作内存。工作内存可以认为是高速缓存。
            主内存存在共享变量,都缓存了isStop


    volatile、synchronized、final、happens-before

    源代码--》编译器的重排序(解决方案: 内存屏障)--》CPU层面的重排序(指令级、内存   解决方案:内存屏障)--》最终执行的指令

    as-if-serial: 不是所有的程序都会进行重排序,要数据依赖规则,不能改变单线程的执行结果:
        int a = 1;
        int b = a;

    被volatile修饰的变量,storeload

4)Happens-Before
    可见性的保障--》volatile以外,还提供了其它方法

    A happens-before B的含义: A操作的结果对于B是可见的,而不是A在B操作之前发生


5)哪些操作会建立Happens-Before
    1.程序的顺序规则
        1 Happens-Before 2   3 Happens-Before 4

    2.volatile
        2 Happens-Before 3     

        一个变量的写操作,一定会对于后面的读操作可见

    3.传递性规则
            1 Happens-Before 2   2 Happens-Before 3     3 Happens-Before 4  一定存在--》1 Happens-Before 4

    4.start规则
        x=10时在main先发生了,再去启动t1线程--》那么t1一定拿到的x是10

    5.join规则
        t1 join了,那么t1中执行完x=100,在main线程打印x一定是100

        t1.join() 会阻塞主线程。。必须等待阻塞释放。 只要存活,则一直wait。 还是基于wait和notify来实现的。

        join的目的是为了:执行结果对后面的程序可见。

    6.锁的规则
        synchronized。。在t1释放锁以后的操作一定在t2加锁之前

6)synchronized: 可以解决原子性、有序性、可见性(使用Happens-Before)

7)volatile: 可以解决可见性。 但是不能解决原子性问题。 禁止指令重排序来打到可见性效果。

你可能感兴趣的:(java)