zoukankan      html  css  js  c++  java
  • Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析

    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。

    volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。

    以下是本文的目录大纲:

    一.内存模型的相关概念

    二.并发编程中的三个概念

    三.Java内存模型

    四..深入剖析volatile关键字

    五.使用volatile关键字的场景

    若有不正之处请多多谅解,并欢迎批评指正。

    请尊重作者劳动成果,转载请标明原文链接:

    http://www.cnblogs.com/dolphin0520/p/3920373.html

    一.内存模型的相关概念

    大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

    也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

    1
    i = i + 1;

    当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

    这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

    比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

    可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

    最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

    也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

    为了解决缓存不一致性问题,通常来说有以下2种解决方法:

    1)通过在总线加LOCK#锁的方式

    2)通过缓存一致性协议

    这2种方式都是硬件层面上提供的方式。

    在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

    但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

    所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

    二.并发编程中的三个概念

    在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:

    1.原子性

    原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

    一个很经典的例子就是银行账户转账问题:

    比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

    试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

    所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

    同样地反映到并发编程中会出现什么结果呢?

    举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

    1
    i = 9;

    假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。

    那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

    2.可见性

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    举个简单的例子,看下面这段代码:

    1
    2
    3
    4
    5
    6
    //线程1执行的代码
    int i = 0;
    i = 10;
     
    //线程2执行的代码
    j = i;

    假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

    此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

    这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

    3.有序性

    有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

    1
    2
    3
    4
    int i = 0;             
    boolean flag = false;
    i = 1;                //语句1 
    flag = true;          //语句2

    上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

    下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

    比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

    但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

    1
    2
    3
    4
    int a = 10;    //语句1
    int r = 2;    //语句2
    a = a + 3;    //语句3
    r = a*a;     //语句4

    这段代码有4个语句,那么可能的一个执行顺序是:

    那么可不可能是这个执行顺序呢: 语句2   语句1    语句4   语句3

    不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

    虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //线程1:
    context = loadContext();   //语句1
    inited = true;             //语句2
     
    //线程2:
    while(!inited ){
      sleep()
    }
    doSomethingwithconfig(context);

    上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

    从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

    也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

    三.Java内存模型

    在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,研究一下Java内存模型为我们提供了哪些保证以及在java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。

    在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

    Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

    举个简单的例子:在java中,执行下面这个语句:

    1
    i  = 10;

    执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

    那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?

    1.原子性

    在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

    上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:

    请分析以下哪些操作是原子性操作:

    1
    2
    3
    4
    x = 10;         //语句1
    y = x;         //语句2
    x++;           //语句3
    x = x + 1;     //语句4

    咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

    语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

    语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

    同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

    所以上面4个语句只有语句1的操作具备原子性。

    也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

    不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。

    从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

    2.可见性

    对于可见性,Java提供了volatile关键字来保证可见性。

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

    另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

    3.有序性

    在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

    在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

    另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

    下面就来具体介绍下happens-before原则(先行发生原则):

    • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
    • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
    • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
    • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
    • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
    • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
    • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
    • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

    这8条原则摘自《深入理解Java虚拟机》。

    这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

    下面我们来解释一下前4条规则:

    对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

    第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

    第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

    第四条规则实际上就是体现happens-before原则具备传递性。

    四.深入剖析volatile关键字

    在前面讲述了很多东西,其实都是为讲述volatile关键字作铺垫,那么接下来我们就进入主题。

    1.volatile关键字的两层语义

    一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

    1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

    2)禁止进行指令重排序。

    先看一段代码,假如线程1先执行,线程2后执行:

    1
    2
    3
    4
    5
    6
    7
    8
    //线程1
    boolean stop = false;
    while(!stop){
        doSomething();
    }
     
    //线程2
    stop = true;

    这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

    下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

    那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

    但是用volatile修饰之后就变得不一样了:

    第一:使用volatile关键字会强制将修改的值立即写入主存;

    第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

    第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

    那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

    那么线程1读取到的就是最新的正确的值。

    2.volatile保证原子性吗?

    从上面知道volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性吗?

    下面看一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Test {
        public volatile int inc = 0;
     
        public void increase() {
            inc++;
        }
     
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
     
            while(Thread.activeCount()>1//保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }

    大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

    可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

    这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

    在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

    假如某个时刻变量inc的值为10,

    线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

    然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

    然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

    那么两个线程分别进行了一次自增操作后,inc只增加了1。

    解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

    根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

    把上面的代码改成以下任何一种都可以达到效果:

    采用synchronized:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Test {
        public  int inc = 0;
     
        public synchronized void increase() {
            inc++;
        }
     
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
     
            while(Thread.activeCount()>1//保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }

    采用Lock:

    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
    public class Test {
        public  int inc = 0;
        Lock lock = new ReentrantLock();
     
        public  void increase() {
            lock.lock();
            try {
                inc++;
            } finally{
                lock.unlock();
            }
        }
     
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
     
            while(Thread.activeCount()>1//保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }

    采用AtomicInteger:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Test {
        public  AtomicInteger inc = new AtomicInteger();
     
        public  void increase() {
            inc.getAndIncrement();
        }
     
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
     
            while(Thread.activeCount()>1//保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }

    在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

    3.volatile能保证有序性吗?

    在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

    volatile关键字禁止指令重排序有两层意思:

    1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

    2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

    可能上面说的比较绕,举个简单的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    //x、y为非volatile变量
    //flag为volatile变量
     
    x = 2;        //语句1
    y = 0;        //语句2
    flag = true//语句3
    x = 4;         //语句4
    y = -1;       //语句5

    由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

    并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

    那么我们回到前面举的一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //线程1:
    context = loadContext();   //语句1
    inited = true;             //语句2
     
    //线程2:
    while(!inited ){
      sleep()
    }
    doSomethingwithconfig(context);

    前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

    这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

    4.volatile的原理和实现机制

    前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

    下面这段话摘自《深入理解Java虚拟机》:

    “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

    lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

    1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

    2)它会强制将对缓存的修改操作立即写入主存;

    3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

    五.使用volatile关键字的场景

    synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

    1)对变量的写操作不依赖于当前值

    2)该变量没有包含在具有其他变量的不变式中

    实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

    事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

    下面列举几个Java中使用volatile的几个场景。

    1.状态标记量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    volatile boolean flag = false;
     
    while(!flag){
        doSomething();
    }
     
    public void setFlag() {
        flag = true;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    volatile boolean inited = false;
    //线程1:
    context = loadContext(); 
    inited = true;           
     
    //线程2:
    while(!inited ){
    sleep()
    }
    doSomethingwithconfig(context);

     

    2.double check

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Singleton{
        private volatile static Singleton instance = null;
     
        private Singleton() {
     
        }
     
        public static Singleton getInstance() {
            if(instance==null) {
                synchronized (Singleton.class) {
                    if(instance==null)
                        instance = new Singleton();
                }
            }
            return instance;
        }
    }

    至于为何需要这么写请参考:

    《Java 中的双重检查(Double-Check)》http://blog.csdn.net/dl88250/article/details/5439024

    http://www.iteye.com/topic/652440

    参考资料:

    Java编程思想

    《深入理解Java虚拟机》

    http://jiangzhengjun.iteye.com/blog/652532

    http://blog.sina.com.cn/s/blog_7bee8dd50101fu8n.html

    http://ifeve.com/volatile/

    http://blog.csdn.net/ccit0519/article/details/11241403

    http://blog.csdn.net/ns_code/article/details/17101369

    http://www.cnblogs.com/kevinwu/archive/2012/05/02/2479464.html

    http://www.cppblog.com/elva/archive/2011/01/21/139019.html

    http://ifeve.com/volatile-array-visiblity/

    http://www.bdqn.cn/news/201312/12579.shtml

    http://exploer.blog.51cto.com/7123589/1193399

    http://www.cnblogs.com/Mainz/p/3556430.html



    相关文章

    发表评论

    Name*
    邮箱*
    网站 (请以 http://开头)

    (*) 表示必填项

    48 条评论

    1. 【解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。】
      针对这一段,我有个疑问请教下博主。当线程2修改inc值后(指写入到主存),不是一样会让线程1中的inc缓存行无效么?这个时候线程1按理来说是要重新到主存中读取inc的值(11),然后再自增1,所以我认为最后值应该还是12。麻烦博主解释下,谢谢

      Well-loved. Like or Dislike: Thumb up 57 Thumb down 2

      • mongoose

        线程1的读取操作是在线程二的写操作之前发生的。以我的理解,这其实执行了下面的伪汇编代码(因为我忘记了汇编代码的具体格式了)

        1) lock ;应该是具体的将主存中的数据读到cache中的操作,我并不知道具体格式是什么
        2) mov ax, 01h ;假设x的值存放在主存的0×01处,但是实际由于通过cpu cache的映射关系,实际是从cache中获得的,只是由于第一句的block指令,让cache中的数据其实和主存中是一样的
        3) inc ax
        4) mov 01h, ax
        5) lock ; cache中的内容写入主存

        以上是线程1的操作,博主的例子是说,线程2的读操作是在1之后,4之前或者5之前,不过这都不重要。你说的线程2对于x的写只要在1之后发生的对于线程1都是无能为力的,因为对于线程1来说,已经没有x了,都是寄存器和自己的cache了。

        Well-loved. Like or Dislike: Thumb up 14 Thumb down 9

      • 一开始这一块我也有疑惑,后来我是这样想的,线程1执行x++,x为10,x++相当于x=10+1,已经使用变量x值算出自增后的值11,但是11未付值给X,然后线程2从内存中读取x为10,进行自增后为11并写入主内存,这个时候线程1中的x已经失效,但是将要给x复制的值11已经算出来了,不再需要x这个变量了,失效不失效无所谓了,完成x=11后写入主内存,导致主内存中应为12的x还是11

        Well-loved. Like or Dislike: Thumb up 64 Thumb down 3

        • 王俊

          厉害了,要真是这样的话,就很好理解了,感谢

          Thumb up 3 Thumb down 0

        • ganwuBug

          还是没明白,如果线程1已经进行了+1的话,不就是进行了修改动作吗,那应该会导致线程2中的缓存无效吧?

          Thumb up 2 Thumb down 2

          • lanqiu5ge

            3)如果是写操作,它会导致其他CPU中对应的缓存行无效。此时线程1并没有写的操作。

            Thumb up 0 Thumb down 0

        • cz

          文章中讲到:使用volatile后会强制将值立即写入到主存中,注意是”立即”,那么线程1将一x的值算出来为11时已存入线程1的缓存中,此时线程一会在阻塞之前将x立即写入到主存中,所以我认为不存在你说的线程一算出了11这个值还没来得及写入主存就交给线程二来处理

          Well-loved. Like or Dislike: Thumb up 4 Thumb down 0

      • 123456

        volatile内存语义有以下两点:

        当对一个volatile变量进行写操作的时候,JMM会把该线程对应的本地内存中的共享变量的值刷新到主内存中。
        当读一个volatile变量的时候,JMM会把该线程对应的本地内存设置为无效,要求线程从主内存中读取数据。

        至于,博客中所说
        第一:使用volatile关键字会强制将修改的值立即写入主存;
        第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
        因为每次循环都要读取boolean变量的值,所以,每次都会去主内存去读,而不是线程的本地内存。这就是volatile的作用

        也就是说,每次读取的时候会保证从主内存读,线程1读取后就不会再去主内存读取了,因为已经读过数据了放在寄存器里,可以直接运算。当线程1再次需要读取的时候,会去主内存读。这是我理解的,可能会帮到你。

        Thumb up 2 Thumb down 0

        • 123456

          缓存行无效设置为无效,是指需要线程读取数据读的时候重新去主内存读,而不是使用数据的时候强制去重新读。因为线程1已经读过了。

          Well-loved. Like or Dislike: Thumb up 7 Thumb down 1

          • cz

            文章中讲到:第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。注意是”再次读取”。你说线程一已经读取了x的值,将其放入了寄存器中,就不再读了,那么volatile的作用究竟是什么,难道只是在写数据时立即将缓存的数据写入主存中?光有这条又有什么意义?

            Thumb up 0 Thumb down 0

        • cz

          你的意思是如果有volatile,线程每次都会从主存中读取数据,但如果没有volatile,线程第一次读取数据肯定是从主存中读到线程的本地内存,但以后每次循环都不从主存读取,还是不都从主存读取?如果是都不肯定是不对的,因为没有volatile时算出来的x值比较大,显然不是每次从自己的内存读取数据。如果是不都就能合理解释了,但问题又来了,没有volatile时,什么时候线程从主存中读取,什么时候从自己的内存中读取?

          Thumb up 0 Thumb down 0

      • tubunanhai

        有一些误导,每个线程都有自己的线程栈,就是所谓线程缓存,但是volatile关键字的使用,只能使线程每次读取共享资源时,所读取的都是最新的,无法使其他线程的什么缓存行无效,每个线程只有权操作自己的线程栈,按题主逻辑,每个线程在改变共享资源的同时会使其他线程对应该共享资源缓存无效,这样每个线程在操作共享资源时,都能读到真实有效的值,得出volatile保证了原子性的结论

        Thumb up 0 Thumb down 1

    2. 讲的真是太详细,受教!

      Well-loved. Like or Dislike: Thumb up 27 Thumb down 1

    3. szdphsw

      那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去

      请问楼主这句话是什么意思,线程2转去做其他事情,难道就不把stop的值写到主存中了吗?

      Well-loved. Like or Dislike: Thumb up 6 Thumb down 2

      • szdphsw

        //线程1
        boolean stop = false;
        while(!stop){
        doSomething();
        }

        //线程2
        stop = true;
        这个例子的说明个人感觉不太对,我认为这个程序一定不会死循环,因为线程2把stop的值修改了,也就是说这个值一定会被写入到内存中,那么线程1肯定会读到这个更新后的值,值不可能慢一点而已。难道楼主的意思是,线程一读取了stop的值后,永远都不会再去内存再次读取了?

        Well-loved. Like or Dislike: Thumb up 17 Thumb down 5

        • cz

          如果线程2还未来的及写入主存就发生了异常行为中止不再运行,那么此时就是永远的死循环

          Thumb up 0 Thumb down 0

    4. zhuhf

      “程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。”
      ——-既然这样了,为何

      //线程1:
      context = loadContext(); //语句1
      inited = true; //语句2

      //线程2:
      while(!inited ){
      sleep()
      }
      doSomethingwithconfig(context);

      “语句2” 会优先于“语句1”执行???

      Well-loved. Like or Dislike: Thumb up 6 Thumb down 2

      • yakunTa

        指令重排序,
        楼主说的很详细了,对一个线程来讲,保证的是执行结果的有序性。
        对线程1来讲,语句1先执行还是语句2先执行,结果都是一样的

        Hot debate. What do you think? Thumb up 6 Thumb down 4

    5. 阿龙

      太棒了,受益匪浅

      Thumb up 2 Thumb down 3

    6. 矽骊

      大赞,费心了

      Thumb up 3 Thumb down 1

    7. liyc

      虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
      //线程1:
      context = loadContext(); //语句1
      inited = true; //语句2

      //线程2:
      while(!inited ){
      sleep()
      }
      doSomethingwithconfig(context);//语句3
      上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

      在执行语句3的时候,语句1不是应该肯定被执行了吗,因为语句3依赖语句1

      Thumb up 3 Thumb down 0

      • 唐尤华

        可以运行以后分析结果

        Thumb up 0 Thumb down 1

      • gds

        所说的依赖,是指的是在一个线程中。在两个线程中,依赖不起作用。这是我的理解

        Thumb up 1 Thumb down 0

      • yangjie

        我也有同样的疑惑,按照语序不影响结果的逻辑,虽然重排序了,但是语句1不是应该始终在语句3之前执行吗?

        Thumb up 1 Thumb down 0

        • 梦中客

          因为这是在两个线程中。
          发生重排后,语句2先于语句1执行,然后线程2就满足条件执行语句3了,当然语句2执行完后就会执行线程1的语句1了。
          这个时候取决于语句1的执行速度是否先于语句3执行完成,因为两个线程中执行的顺序是不一定的,有可能语句1线执行完成再执行语句3,有可能执行语句3的时候语句1还没执行。
          所以可以这么说,这个不是绝对会出现问题,但是有可能会发生问题,是有缺陷的设计

          Thumb up 0 Thumb down 0

    8. 周慷

      文章:四.深入剖析volatile关键字 ,1.volatile关键字的两层语义 处,
      关于stop那个例子造成死循环的原因是指令重排引起的,文章解释错误,望及时修改!

      Thumb up 1 Thumb down 0

    9. 不喜欢吃冬瓜

      最后那个单例的例子没太明白,既然都使用双重校验锁了,为什么还要用volatile修饰?那个instance是不可变的

      Thumb up 3 Thumb down 0

      • 孙克

        +1,同问。

        Thumb up 1 Thumb down 0

      • 孙克

        +1,同问。也给楼主点个赞,讲解很细,demo很好!

        Thumb up 2 Thumb down 0

      • motian

        因为有指令重排,赋值操作不是原子操作

        Thumb up 2 Thumb down 0

      • 渣wa

        问出这个问题,应该是你还是考虑的单线程场景,或者对DCL单例创建的意义不太明白
        首先要确定volatile变量在此处的意义是 “”禁止指令重排序“”,你要理解这个指令重排序的指令是原子操作的指令
        DCL单例中:
        instance = new Single();这样一个语句多个指令,譬如先执行构造函数,后赋值,
        如果没有volatile,则此处可能会发生,先赋值,后执行构造函数,极端假设:构造函数执行很慢
        在这个漫长的过程中,如果此时,另外的CPU2访问到这里
        if(instance==null) {
        synchronized (Singleton.class) {
        if(instance==null)
        instance = new Singleton();
        }
        }
        这个对象是存在的,因为 先赋值了,但是构造函数没有执行完毕,所以CPU2号则会报告初始化错误;

        如果有了volatile变量进行修饰,则会禁止指令重排,编译后,多了一个
        lock add1 ..(%esp) 具体忘记了。。。。,相当于一个内存屏障,会保证同步在内存之前,之前的操作全部完成,所以有指令重排序无效的效果,具体去看深入JVM那本书吧,已经快忘记了

        如有不对,欢迎大神讨论和Review上面的话

        Thumb up 0 Thumb down 0

    10. liubangweihaha

      还是比较透彻、全面的

      Thumb up 1 Thumb down 0

    11. public class Test {
      public volatile int inc = 0;

      public void increase() {
      inc++;
      }

      public static void main(String[] args) {
      final Test test = new Test();
      for(int i=0;i<10;i++){
      new Thread(){
      public void run() {
      for(int j=0;j1) //保证前面的线程都执行完
      Thread.yield();
      System.out.println(test.inc);
      }
      }

      为什么我执行这个例子结果并没有出现博主所说的效果呢,每次运行的值都是10000.

      Thumb up 0 Thumb down 0

    12. Lin

      高速缓存错了吧?这个不是硬件层面的缓存一致性协议保证的吗?
      这里多线程是作用于jvm里的内存模型里的工作内存吧?

      Thumb up 1 Thumb down 0

    13. 采用AtomicInteger的那个方法测试了一下,无论在inc上加不加volatile,执行结果依然小于10000

      Thumb up 0 Thumb down 0

      • 测试完成,这段代码并不能保证线程执行完毕
        while(Thread.activeCount()>1) //保证前面的线程都执行完
        Thread.yield();
        System.out.println(test.inc);

        Thumb up 2 Thumb down 0

    14. 一月的雨

      试试重排序不会影响单个线程内程序执行的结果

      Thumb up 0 Thumb down 1

    15. li

      1
      2
      3
      4
      5
      6
      7
      8
      9
      //线程1:
      context = loadContext(); //语句1
      inited = true; //语句2

      //线程2:
      while(!inited ){
      sleep()
      }
      doSomethingwithconfig(context);
      前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

      这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。
      对于这段,context在线程一中初始化完成,线程二就一定可以看到?有没有可能线程二看到的context不是最新的

      Thumb up 1 Thumb down 0

    16. iit

      本人最近学习多线程,学到volatile这也有点疑惑,麻烦楼主有时间时帮忙解答一下,先行谢过!就是i++不是原子性的这块,线程1先加载了i,此时没有修改呢,此时线程2也加载了i然后修改了,楼主的意思由于线程1已经加载了i,所以线程2修改后同步到主内存并不会影响到线程1已加载的值,但volatile不是说修改后立即对其它线程可见吗? 是不是这句话说的概念范围有点大?这里面所说的其它线程是不是应该不包括已经加载了i值的线程? 也就是说 把这句话改为 对其它未加载i值的线程立即可见? 还请楼主有时间时帮忙解惑,确实想不明白

      Thumb up 3 Thumb down 1

    17. 明白了不少 受教了

      Thumb up 0 Thumb down 0

    18. SLINCEOB

      讲的很详细,虽然里面很多东西现在看的还是一知半解。感谢!!!

      Thumb up 0 Thumb down 0

    19. 韩超

      谢谢分享!

      Thumb up 0 Thumb down 0

    20. 胡方节

      多和CPU我不清楚怎么做的,但是在单核CPU下面,楼主一开始讲的就有点问题吧。
      主存和CPU之间有一个高速缓存,主存把一部分数据映射到高速缓存中。现在假设有两个线程A和B,由于线程
      A和B是同一个进程的,故而共享同一个虚拟地址空间。当线程A将某个数据写入虚拟地址x处的内存时,操作系统会先看告诉缓存中是否存在这个内存地址的映射,如果存在,则写入。此时线程B也要读取虚拟地址x的数据,同样操作系统查看是否有映射,如果有映射,则直接读取刚才A写入的值,不可能说A将值写入地址x处时,写到的是告诉缓存中,而B读取x处地址的值时是从内存中读的。操作系统不会这么干。至于告诉缓存和内存的同步,是在发生块交还的时候,通过写回机制实现的,和你说的这个volatile关键字的作用是两码事情。

      Thumb up 0 Thumb down 0

    21. jackwang

      当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
      关于这句话,不是特别理解,线程2在更新了本地副本stop变量的值后,是什么决定在什么时机将这个值更新到主存中去的呢?(我想表达的是,如果不用volatile修饰,主存中的值就一定不能保证得到及时的更新呢?)

      Thumb up 0 Thumb down 0

  • 相关阅读:
    在同一台机器上让Microsoft SQL Server 2000/ SQL2005/ SQL2008共存
    HTML 中<style>中</style>里面<!-- -->标签是干嘛的
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    怎样清除td和input之间空隙
    aspx页面中, <%= % > 与 <%# % > 的区别
    python之day13(ORM,paramiko模块,堡垒机)
    python之day12(线程池,redis,rabbitMQ)
    RabbitMQ与SQLAlchemy(预习)
    redis操作篇
    python之day11(线程,进程,协程,memcache,redis)
  • 原文地址:https://www.cnblogs.com/LinQingYang/p/11875696.html
Copyright © 2011-2022 走看看