zoukankan      html  css  js  c++  java
  • volatile、Synchronized实现变量可见性的原理,volatile使用注意事项

    变量不可见的两个原因

    Java每个线程工作都有一个工作空间,需要的变量都是从主存中加载进来的。Java内存模型如下(JMM):

    线程访问一个共享的变量时,都需要先从主存中加载一个副本到自己的工作内存中,经过自己修改后再更新到主存中去。在这个过程中可能出现这种情况:线程A在工作内存中修改了变量1的值,但是还没有写入主存,这档口线程B将变量1加载到自己工作内存中。显然,线程B拿到的不是变量1的最新值了。

    变量可见性就是: 这个变量被任何一个线程修改了,其他线程都能“看见”,也就是能取到变量最新的值。

    重排序是指为了适合cpu指令执行机制,编译器、内存系统、处理器可能会对一些指令的执行顺序进行重排。例如:

    int a = 1;               //line1
    int b = 2;                //line2
    int s = a*b;        //line3

    line1 和 line2 可能会被重排颠倒位置,但是line3不会重排,因为Java单线程下会遵守 as-if-serial语义,简单的讲就是重排指令不会出现错误的结果。在多线程下,指令重排则可能造成一些问题,例如:

    class Example {
    int a = 0;
    boolean flag = false;
     
    public void writer() {
        a = 1;                   
        flag = true;           
    }
     
    public void reader() {
        if (flag) {                
            int i =  a +1;      
        }
    }
    }

    线程A首先执行writer()方法,线程B线程接着执行reader()方法。线程B在int i=a+1 时不一定能看到a已经被赋值为1,因为在writer()中,两句话顺序可能打乱:

    线程A执行顺序:  flag=true;(a=1;还没执行完或还没写到主存)

    线程B执行:flag=true  (而此时a=0)    产生了一些与我们预期之外的情况。

    导致变量不可见的原因(1)更新不及时,(2)多线程交替执行时的指令重排序

    volatile实现可见性的原理

    JVM线程工作时的原子性指令有:

        read: 从主存读取一个变量的值的副本到线程的工作内存。

        load:把read来的值赋给工作空间的变量中,然后就可以使用了。

        use:要使用一个变量,先发出这个指令。

        assign:赋值,给变量一个新值。

        store:将工作空间的变量值运送到主存中。

        write:将值写到主存的那个变量中。

    上述操作必定是顺序执行的,但可不一定连续,中间可能插入其他指令。为了保证可见性:关键就是保证load、use的执行顺序不被打乱(保证使用变量前一定刚刚进行了load操作,从主存拿最新值来),assign、wirte的执行顺序不被打乱(保证赋值后马上就是把值写到主存)。

    所以使用内存屏障, CPU指令,可以禁止指令执行乱序:插入一个内存屏障, 相当于告诉CPU和编译器指令顺序先于这个指令的必须先执行,后于这个命令的必须后执行。

    解决第一个导致不可见的因素(更新不及时):内存屏障,对于volatile修饰的变量,读操作时在读指令use插入一条读屏障指令重新从主存加载最新值进来,保证了load、use指令的执行顺序不乱;写操作时在写指令assign插入一条写屏障指令,将工作内存变量的最新值立刻写入主存变量。

    解决第二个因素(指令重排): 由于读写数据时会在之前/后插入一条内存屏障指令,因此volatile可以禁止指令重排序。

    Synchronized实现可见性原理

    解决第一个因素:在加锁前会将工作内存的值全部重新加载一遍,保证最新;释放锁前将工作内存的值全部更新到主存;由于在带锁期间,没有其他线程能访问本线程正在使用的共享变量,这样就保证了可见性。

    解决第二个因素: 由于Synchronized修饰的代码块都是原子性执行的,即一旦开始做,就会一直执行完毕,期间有其他线程不可以访问本线程所使用的共享变量,这样,即便指令重排了也不会出现问题。

    volatile不具有原子性可能导致的问

    经过前面的总结,可以看出Synchronized包裹的代码里的共享变量有可见性、指令不可排序性、原子性;volatile修饰的变量有可见性、指令不可排序性;volatile并不保证变量有原子性。如果用volatile修饰变量希望保证它的原子性就可能出现问题,例如:

    volatile  int num = 0;
    
    线程A:
          num++;
    
    线程B:
          num++;

    两个线程操作都进行num++的操作,理论上完事后 num 的值为2,但是num++ 的操作本身不是原子性的(包括读取 num原先的值、+1、把+1后的值写入num),volatile也不能使对num的操作变为原子性。因此可能有num = 1的结果:

           线程A:读取了num的值,为0,然后阻塞了。

           线程B:读取num的值,还是为0,+1后立即写入主存,num = 1(体现可见性)。

            线程A:恢复执行了,已经做完读取操作,工作内存中num = 0,继续执行。num+1,写入内存,num = 1;

    虽然这种情况发生的概率很小,但是并发量大时还是会出现不少这种情况的(可以用Synchronized、lock、AtomicInteger等解决)。

    由于原子性的问题,volatile不适合修饰依赖自身的变量 :  num++、a = a*2...    也不适合修饰不变式的变量(即volatile变量应该是独立的):a<b.... 

  • 相关阅读:
    ZROI2018提高day5t1
    noi.ac day1t1 candy
    ARC102E Stop. Otherwise...
    TOP命令详解(负载情况)
    mysql 时间函数 时间转换函数
    maven编译时错误:无效的目标发行版
    jsp base路径
    mybatis typehandler
    终极解决方案 at org.apache.jsp.index_jsp._jspInit(index_jsp.java:22) 报空指针
    【转】 IntelliJ IDEA像Eclipse一样打开多个项目
  • 原文地址:https://www.cnblogs.com/shen-qian/p/11250805.html
Copyright © 2011-2022 走看看