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.... 

  • 相关阅读:
    eclipse下c/cpp " undefined reference to " or "launch failed binary not found"问题
    blockdev 设置文件预读大小
    宝宝语录
    CentOS修改主机名(hostname)
    subprocess报No such file or directory
    用ldap方式访问AD域的的错误解释
    英特尔的VTd技术是什么?
    This virtual machine requires the VMware keyboard support driver which is not installed
    Linux内核的文件预读详细详解
    UNP总结 Chapter 26~29 线程、IP选项、原始套接字、数据链路访问
  • 原文地址:https://www.cnblogs.com/shen-qian/p/11250805.html
Copyright © 2011-2022 走看看