zoukankan      html  css  js  c++  java
  • 从jmm模型漫谈到happens-befor原则

    首先,代码都没有用ide敲,所以不要在意格式,能看懂就行
    jmm内存模型:
    jmm是什么?

    jmm说白了就是定义了jvm中线程和主内存之间的抽象关系的一种模型,也就是线程之间的共享变量存储在主内存,而每个线程都拥有自己的工作内存

    happens-befor原则是什么?

    在说happens-befor原则之前,我们得先说说jmm的问题所在,如上述所述,每个线程都有自己的一个工作内存,那么我们以一个代码实例来看

    public class Test{
    int a=1;
    public static void main(String []args){
    for(int i=0;i<=100000;i++){
    new Thread(()->{

    a++;

    }).start();

    }
    system.out.println(a);
    }

    }
    OK,大家能看到,这里是有一个开启了100000个线程做自增操作,结果是99130,并不是预期中的100000,那么这是为什么呢?大家都知道,线程是CPU运行的最小单元,那么也就是说,多线程的情况下,cpu会去随机运行的(哪怕是设置了优先级也只是一个权重问题,无法保证强顺序
    并且任务一定执行完),所以,因为我们的a++并不是一个原子操作的原因(实际上是4步),也就是说,很可能面临这种情况,当一个线程拿到了cpu的时间切片的时候,首先cpu会将a读到内存中,此时假设a=1然后弄一个临时变量,之后将临时变量增加,之后再返回结构到主内存,但如果在创建了
    临时变量但还没有做自增操作的时候,cpu的时间切片突然换到了另一个线程的上面,这个时候这个线程成功做完了自增操作,此时a=2,之后cpu又切回了之前的线程,因为线程里有一个程序计数器,记录了当前线程运行到了哪行代码,所以这个时候第一个线程继续做+1操作,但此时
    由于第一个线程的工作内存里的a还是1,所以这个时候线程a在+1之后还是2,之后刷到主内存,此时a=2.所以这两个线程虽然各自运行了一次a++操作,但主内存里的a其实只是加了一次而已.
    那么怎么避免这种情况呢?此时就需要我们的happens-befor原则了

    happens-befor原则:
    1:程序在运行的时候必须按照编写的顺序一样,不能进行指令重排序,指令重排会导致什么后果呢?
    public class Test{
    int a = 0;
    boolean b = false;
    public void write(){
    a=1;
    b = true;
    }
    public void read(){
    if(b){
    a = a+1;
    }
    }
    }
    而指令重排后可能会是
    public class Test{
    int a = 0;
    boolean b = false;
    public void write(){
    b = true;
    a=1;

    }
    public void read(){
    if(b){
    a = a+1;
    }
    }
    }
    如果此时有两个线程
    new Thread(->(
    write();
    )).start();
    new Thread(->{
    read();
    }).stert();
    假设write()方法线程肯定先于read()方法执行的情况下,此时可能会导致,在b=true的时候,read方法进入,并导致a最后=1,但我们代码的原意其实a=1优先执行的话,a=1的情况会因为read方法的bool没有变成true所以无法进入,因此指令重排已经
    干扰到了我们的代码原意了.

    那么在什么情况下指令不会重排呢?两种,一种是上下代码有依赖关系如:
    int a = 1;
    int b = a+1;
    此时就不会出现重排;
    还有一种就是使用大名鼎鼎的volatile关键字,它利用了内存屏障的性质保证了指令不会重排,最后来点拓展知识,就是long和double这种64字节的数据类型,在读到工作内存的时候不会原子性的,而是每次只读32字节,最后分两次读,但如果加了volatile关键字的话,那么内存屏障
    能保证一次性读完64字节.

    2:一个锁的解锁,必须要在这个程序的加锁之前,也就是说,我不解锁,那你就别想再加锁,保证了串行
    3:对于共享数据,上一个线程对于它的修改,必须要对后续任意操作它的线程可见
    4:传递性,假设有三个操作,a,b,c可以理解为a happens befor b,b happens befor c ,那么a happens befor c;

    OK,volatile除了内存屏障之外,其实还有另一个作用,就是保证了可见性,它是怎么保证的呢?其实就是将工作内存给去掉,也就是让每次cpu读数据都必须要主内存里里拿,就这样保证在一个线程修改了数据后,他对所有线程都是可见的.可惜的是,这种可见性也并不能保证线程安全,因为线程安全需要两个保证,一个可见性,还有一个原子性.
    假设现有一个线程1,一个线程2,一个共享变量a=1,此时线程1将a拿到工作内存做a++操作,在它还没有返回的期间线程b也拿了a到工作内存做++操作,之后不管谁先返回,a都只是做了一次操作而已.所以volatile只能保证那些赋值操作的线程安全,如:Boolean bool = true;

    总而言之,volatile的作用就是在操作之间建立happens-befor的关系

    最后,推荐大家看下CAS的源码,利用volatile加乐观锁,实现了不需要synchronized也能保证线程安全.

  • 相关阅读:
    window对象的方法
    JS注册事件
    JS总结
    JS 扩展方法prototype
    Codeforces 460D Little Victor and Set(看题解)
    Codeforces 891C Envy
    Codeforces 251C Number Transformation
    Codeforces 490F Treeland Tour 树形dp
    Codeforces 605C Freelancer's Dreams 凸包 (看题解)
    几何模板
  • 原文地址:https://www.cnblogs.com/yangfeiORfeiyang/p/9369146.html
Copyright © 2011-2022 走看看