• 并发编程day03


    一个问题引发的思考

    public static void main(String[]args)throws InterruptedException{
    Thread thread=new Thread(()->{
    int i=0;
    while(!stop){
    i++;
    //System.out.println("rs:"+i);
    try
    {
    Thread.sleep(0);
    }
    catch
    (InterruptedException e){
    e.printStackTrace();
    }
    }
    });
    thread.start();
    Thread.sleep(1000);
    stop=true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    print就可以导致循环结束

    活性失败
    .
    JIT深度优化

    while(!stop){
    i++;
    //System.out.println("rs:"+i);
    try
    {
    Thread.sleep(0);
    }
    catch(InterruptedException e){
    e.printStackTrace();
    }
    }
    if(!stop){
    while(true){
    i++;
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这里分为两个层面来解答println底层用到了synchronized这个同步关键字,这个同步会防止循环期间对于stop值的缓存。
    因为println有加锁的操作,而释放锁的操作,会强制性的把工作内存中涉及到的写操作同步到主内存,
    可以通过如下代码去证明。

    Thread thread=new 
    Thread(()->{
    int i=0;
    while(!stop){
    i++;
    synchronized(JITDemo.class){
    }
    }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第三个角度,
    从IO角度来说,print本质上是一个IO的操作,我们知道磁盘IO的效率一定要比CPU的计算效率慢得多,所以IO可以使得CPU有时间去做内存刷新的事情,从而导致这个现象。比如我们可以在里面定义一个new File()。同样会达到效果。

    Thread.sleep(0)

    https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.3
    在这段代码中,我们增加Thread.sleep(0)也能生效,这个我认为是和cpu、以及jvm、操作系统等因素
    有关系。
    官方文档上是说,Thread.sleep没有任何同步语义,编译器不需要在调用Thread.sleep之前把缓存在寄
    存器中的写刷新到给共享内存、也不需要在Thread.sleep之后重新加载缓存在寄存器中的值。编译器可以自由选择读取stop的值一次或者多次,这个是由编译器自己来决定的。但是在认为:Thread.sleep(0)导致线程切换,线程切换会导致缓存失效从而读取到了新的值。
    volatile关键字(保证可见性)
    通过对上述代码查看汇编指令,使用HSDIS工具,具体的使用方法详见压缩文档。
    可以看到,使用volatile关键字之后,多了一个Lock指令。
    0x00000000037028f3:lock add dword ptr[rsp],0h;*putstatic stop
    0x0000000002b7ddab:push 0ffffffffc4834800h;*putstaticstop
    ;-
    com.example.threaddemo.VolatileDemo::<
    思考
    lock汇编指令来保证可见性问题?

    什么是可见性

    在单线程的环境下,如果向一个变量先写入一个值,然后再没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值,这本来是一个正常的事情。但是再多线程环境下,读和写发生再不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最新值,这就是所谓的可见性。

    硬件层面

    CPU/内存/IO设备
    CPU层面增加了高速缓存
    操作系统,进程、线程、|CPU时间片来切换
    编译器的优化,更合理的利用CPU的高速缓存
    .CPU层面的高速缓存
    因为高速缓存的存在,会导致一个缓存一致性问题

    总线锁&缓存锁

    总线锁,简单来说就是,在多cpu下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出
    一个LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把
    CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,这种机制显然是不合适的。
    如何优化呢?最好的方法就是控制锁的保护粒度,我们只需要保证对于被多个CPU缓存的同一份数据是
    一致的就行。在P6架构的CPU后,引入了缓存锁,如果当前数据已经被CPU缓存了,并且是要协会到主内存中的,就可以采用缓存锁来解决问题。所谓的缓存锁,就是指内存区域如果被缓存在处理器的缓存行中,并且在Lock期间被锁定,那么当它执行锁操作回写到内存时,不再总线上加锁,而是修改内部的内存地址,基于缓存一致性协议来保证操作的原子性。
    总线锁和缓存锁怎么选择,取决于很多因素,
    比如CPU是否支持以及存在无法缓存的数据时(比较大或者快约多个缓存行的数据),必然还是会使用总线锁。

    缓存一致性协议

    MSI,MESI、MOSI …
    为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,
    常见的协议有MSI,MESI,MOSI等。最常见的就是MESI协议。接下来给大家简单讲解一下MESI
    MESI
    表示缓存行的四种状态,分别是
    1.M(Modify)表示共享数据只缓存在当前CPU缓存中,并且是被修改状态,也就是缓存的数据和主内
    存中的数据不一致
    2. E(Exclusive)表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改
    3. S(Shared)表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存数据一致
    4. I(Invalid)表示缓存已经失效

    引出了MESI的一个优化

    Store Buferes
    Store Buferes是一个写的缓冲,对于上述描述的情况,CPU0可以先把写入的操作先存储到Store
    Buferes中,Store Buferes中的指令再按照缓存一致性协议去发起其他CPU缓存行的失效。而同步来
    说CPU0可以不用等到Acknowledgement,继续往下执行其他指令,直到收到CPU0收到
    Acknowledgement再更新到缓存,再从缓存同步到主内存。

    指令重排序

    我们来关注下面这段代码,假设分别有两个线程,分别执行executeToCPU0和executeToCPU1,
    分别由两个不同的CPU来执行。
    引入Store Buferes之后,就可能出现b1返回true,但是assert(a1)返回false。很多同学肯定会表
    示不理解,这种情况怎么可能成立?那接下来我们去分析一下。

    executeToCPU0(){
    a=1;
    b=1;
    }
    executeToCPU1(){
    while(b==1){
    assert(a==1);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    通过内存屏障禁止了指令重排序
    X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)
    Store Memory Barrier(写屏障),告诉处理器在写屏障之前的所有已经存储在存储缓存(store buferes)
    中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或者写是可见的
    Load Memory Barrier(读屏障),处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏
    障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的
    Full Memory Barrier(全屏障),确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障
    后的读写操作

    volatile int a=0;
    executeToCpu0(){
    a=1;
    //storeMemoryBarrier()
    写屏障,
    写入到内存
    b=1;
    //
    CPU
    层面的重排序
    //b=1;
    //a=1;
    }
    executeToCpu1(){
    while(b==1){
    //true
    loadMemoryBarrier()
    ;
    //
    读屏障
    assert(a==1)
    //false
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    软件层面

    volatile int a=0;
    executeToCpu0(){
    a=1;
    storeload();
    b=1;
    }
    executeToCpu1(){
    while(b==1){
    assert(a==1)
    //false
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    JMM

    简单来说,
    JMM定义了共享内存中多线程程序读写操作的行为规范:在虚拟机中把共享变量存储到内存以及从内存中取出共享变量的底层实现细节。通过这些规则来规范对内存的读写操作从而保证指令的正确性,它解决了CPU多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的可见性。

    需要注意的是,JMM并没有主动限制执行引擎使用处理器的寄存器和高速缓存来提升指令执行速度,也没主动限制编译器对于指令的重排序,也就是说在JMM这个模型之上,仍然会存在缓存一致性问题和指令重排序问题。JMM是一个抽象模型,它是建立在不同的操作系统和硬件层面之上对问题进行了统一的抽象,然后再Java层面提供了一些高级指令,让用户选择在合适的时候去引入这些高级指令来解决可见性问题。

    JMM是如何解决可见性和有序性问题的

    其实通过前面的内容分析我们发现,导致可见性问题有两个因素,一个是高速缓存导致的可见性问题,
    另一个是指令重排序。那JMM是如何解决可见性和有序性问题的呢?
    其实前面在分析硬件层面的内容时,已经提到过了,对于缓存一致性问题,有总线锁和缓存锁,缓存锁
    是基于MESI协议。而对于指令重排序,硬件层面提供了内存屏障指令。而JMM在这个基础上提供了
    volatile、final等关键字,使得开发者可以在合适的时候增加相应相应的关键字来禁止高速缓存和禁止
    指令重排序来解决可见性和有序性问题。

    Volatile的原理

    通过javap -v VolatileDemo.class

    public static volatile booleanstop;
    descriptor: Z
    flags:ACC_PUBLIC,ACC_STATIC,ACC_VOLATILE
    int field_offset=cache->f2_as_index();
    if(cache->is_volatile()){
    if(tos_type==itos){
    obj->release_int_field_put(field_offset,
    STACK_INT(-1));
    }else if(tos_type==atos){
    VERIFY_OOP(STACK_OBJECT(-1));
    obj->release_obj_field_put(field_offset,
    STACK_OBJECT(-1));
    OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj>>
    CardTableModRefBS::card_shift],0);
    }else if(tos_type==btos){
    obj->release_byte_field_put(field_offset,
    STACK_INT(-1));
    }else if(tos_type==ltos){
    obj->release_long_field_put(field_offset,
    STACK_LONG(-1));
    }else if(tos_type==ctos){
    obj->release_char_field_put(field_offset,
    STACK_INT(-1));
    }else if(tos_type==stos){
    obj->release_short_field_put(field_offset,
    STACK_INT(-1));
    }else if(tos_type==ftos){
    obj->release_float_field_put(field_offset,
    STACK_FLOAT(-1));
    }else{
    obj->release_double_field_put(field_offset,
    STACK_DOUBLE(-1));
    }
    OrderAccess::storeload();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    Happens-Before模型

    除了显示引用volatile关键字能够保证可见性以外,在Java中,还有很多的可见性保障的规则。从JDK1.5开始,引入了一个happens-before的概念来阐述多个线程操作共享变量的可见性问题。
    所以我们可以认为在JMM中,如果一个操作执行的结果需要对另一个操作课件,那么这两个操作必须要存在happens-before关系。这两个操作可以是同一个线程,也可以是不同的线程。

    程序顺序规则(as-if-serial语义)

    不能改变程序的执行结果(在单线程环境下,执行的结果不变.)
    依赖问题,如果两个指令存在依赖关系,是不允许重排序
    int a=0;
    int b=0;
    void test(){
    int a=1; a
    int b=1; b
    //int b=1;
    //int a=1;
    int c=a*b; c
    }
    a happens -before b ;
    b happens before c

    传递性规则

    a happens-before b , b happens- before c,
    a happens-before c

    volatile变量规则

    volatile修饰的变量的写操作,
    一定happens-before后续对于volatile变量的读操作
    .
    内存屏障机制来防止指令重排

    public
    class
    VolatileExample{
    int a=0;
    volatile boolean flag=false;
    public void writer(){
    a=1; 1
    flag=true;
    //修改 2
    }
    public void reader(){
    if(flag){
    //true 3
    int i=a;//1  4
    }
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1happens-before 2
    是否成立?是-> ?
    3 happens-before 4
    是否成立?是
    2 happens -before 3 ->volatile规则
    1happens-before 4 ;
    i=1成立

    监视器锁规则

    int x=10;
    synchronized(this){
    //后续线程读取到的x的值一定12
    if(x<12){
    x=12;
    }
    }
    x=12;

    start规则

    public class StartDemo{
    int x=0;
    Thread t1=newThread(()->{
    //读取x的值 一定是20
    if(x==20){
    }
    })
    ;
    x=20;
    t1.start()
    ;
    }

    Join规则

    public class Test{
    int x=0;
    Thread t1=new Thread(()->{
    x=200;
    });
    t1.start();
    t1.join();
    //保证结果的可见性。
    //在此处读取到的x的值一定是200.
    }
    final关键字提供了内存屏障的规则
    .

  • 相关阅读:
    自学雅思的教程
    vue3自定义开发酒店房态时间组件
    云原生K8S精选的分布式可靠的键值存储etcd原理和实践
    jQuery【jQuery树遍历、jQuery动画(一)、jQuery动画(二)】(四)-全面详解(学习总结---从入门到深化)
    ES6 从入门到精通 # 20:async 的用法
    python基础语法(五)
    Keil C51与Keil MDK的兼容安装
    【JUC】8.ThreadLocal
    go使用grpc
    es(Elasticsearch)客户端Elasticsearch-head安装使用(03Elasticsearch-head安装篇)
  • 原文地址:https://blog.csdn.net/weixin_49349744/article/details/125507896