Java并发编程:从源码分析volatile关键字的实现关键字场景2.内存模型相关概念Cache一致性问题。这种被多个线程访问的变量通常称为共享变量。也就是说,如果一个变量缓存在多个CPU中(通常只出现在多线程编程中),那么就可能存在缓存不一致的问题。为了解决缓存不一致的问题,一般来说有以下两种解决方案:通过在总线上加LOCK#锁通过缓存一致性协议,这两种方法是在硬件层面提供的。上面的方法1会存在一个问题,因为其他CPU在总线锁定期间无法访问内存,导致效率低下。缓存一致性协议。最著名的是Intel的MESI协议,它保证了每个缓存中使用的共享变量的副本是一致的。它的核心思想是:当CPU写入数据时,如果发现正在操作的变量是一个共享变量,即在其他CPU中有该变量的副本,则发送信号通知其他CPU使该变量失效变量的缓存行。因此,当其他CPU需要读取这个变量时,发现自己的缓存中缓存该变量的缓存行无效时,就会重新从内存中读取。3.并发编程中的三个概念在并发编程中,我们通常会遇到以下三个问题:原子性、可见性和顺序。3.1原子性原子性:即一个操作或多个操作要么全部执行并且执行过程不会被任何因素打断,要么根本不执行。3.2可见性可见性是指当多个线程访问同一个变量时,一个线程修改变量的值,其他线程可以立即看到修改后的值。3.3有序性有序性:即程序执行的顺序遵循代码执行的顺序。从代码顺序来看,statement1在statement2之前,那么JVM在真正执行这段代码的时候,会保证statement1先于statement2执行吗?不一定,为什么?这里可能存在指令重复排序(InstructionReorder)。让我解释一下什么是指令重新排序。一般来说,为了提高程序运行的效率,处理器可能会对输入的代码进行优化。它不保证程序中每条语句的执行顺序与代码中的顺序一致,但会保证程序最终的执行结果与代码的顺序执行一致。指令重排序不会影响单个线程的执行,但会影响线程并发执行的正确性。也就是说,要让并发程序正确执行,必须保证原子性、可见性和顺序。只要一个不保证,就可能导致程序运行不正常。4、Java内存模型在Java虚拟机规范中,试图定义一种Java内存模型(JavaMemoryModel,JMM)来屏蔽各种硬件平台和操作系统的内存访问差异,使Java程序可以运行在各种平台。达到一致的内存访问效果。那么Java内存模型是怎么规定的呢?它定义了程序中变量的访问规则,更大程度上定义了程序执行的顺序。注意,为了获得更好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或缓存来提高指令执行速度,也没有限制编译器对指令进行重新排序。也就是说,在java内存模型中,还会存在缓存一致性和指令重排序的问题。Java内存模型规定所有的变量都存放在主内存中(类似于上面提到的物理内存),每个线程都有自己的工作内存(类似于前面的缓存)。线程对变量的所有操作都必须在工作内存中进行,不能直接对主内存进行操作。并且每个线程不能访问其他线程的工作内存。4.1原子性在Java中,基本数据类型变量的读取和赋值操作都是原子操作,即这些操作不能被中断,不能被执行或不被执行。请分析以下哪些操作是原子操作:x=10;//语句1y=x;//语句2x++;//语句3x=x+1;//语句4其实只有语句1是原子操作,其他三个语句都不是原子操作。也就是说,只有简单的读取和赋值(而且必须给一个变量赋一个数,变量之间的相互赋值不是原子操作)才是原子操作。从上面可以看出,Java内存模型只保证了基本的读取和赋值都是原子操作。如果想实现更大范围操作的原子性,可以使用synchronized和Lock来实现。4.2可见性对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修改时,它会保证修改后的值会立即更新到主存中,当其他线程需要读取它时,它会去内存中读取新的值。但是普通共享变量不能保证可见性,因为普通共享变量修改后,什么时候写入主存是不确定的,当其他线程读取它们时,此时内存可能还是原来的旧值,所以Visibility不能得到保证。此外,还可以通过synchronized和Lock来保证可见性。synchronized和Lock可以保证同一时刻只有一个线程获取锁然后执行同步代码,在锁释放前对变量的修改会刷新到主存。所以能见度是有保证的。4.3排序在Java内存模型中,允许编译器和处理器对指令进行重新排序,但重新排序的过程不会影响单线程程序的执行,但会影响多线程并发执行的正确性。在Java中,可以通过volatile关键字来保证一定的“顺序”(它可以禁止指令重新排序)。另外可以使用synchronized和Lock来保证有序性。显然,synchronized和Lock保证每一时刻一个线程执行同步代码,相当于让线程顺序执行同步代码,自然保证了有序性。另外,Java内存模型有一些先天的“有序性”,即不需要任何手段就可以保证有序性。这通常称为happens-before原则。如果两个操作的执行顺序不能从happens-before原则推导出来,那么它们就不能保证它们的顺序,虚拟机可以随意重新排序。下面详细介绍happens-before原则:程序顺序规则:在一个线程中,按照代码顺序,写在前面的操作发生在写在后面的操作之前加锁规则:一个unLock操作发生在前面后面的相同的锁操作volatile变量规则:对一个变量的写操作先发生在该变量的读操作之后传输规则:如果操作A在操作B之前发生,并且操作B在操作C之前发生,则可以得出操作A先发生在操作之前C.线程启动规则:Thread对象的start()方法对于本线程的每一个动作都先发生。线程中断规则:线程interrupt()方法的调用先发生在线程被调用之前中断线程的代码检测中断事件的发生。线程终止规则:线程中的所有操作都发生在线程终止检测之前。我们可以通过Thread.join()方法和Thread.isAlive()的返回值来检测线程是否结束了,线程已经终止执行对象终结规则:一个对象的初始化先发生在它的finalize开始的时候()方法。在这8条规则中,前4条比较重要,后4条都是显而易见的。先解释一下前4条规则:对于程序顺序规则,我的理解是一段程序代码的执行在单线程中看起来是有序的。注意,虽然这条规则提到“写在前面的操作先发生在写在后面的操作”,但这应该是程序看起来执行的顺序是按照代码的顺序执行的,因为虚拟机可以执行程序代码指令重新排序。虽然进行了重排序,但最终的执行结果与程序的顺序执行是一致的,只会对不存在数据依赖的指令进行重排序。因此,在单线程中,程序执行看起来是按顺序执行的,这一点要慎重理解。其实这个规则是用来保证程序在单线程中执行结果的正确性,但是并不能保证程序在多线程中执行的正确性。第二条规则也比较容易理解,也就是说,无论是在单线程还是多线程中,如果加锁了同一个锁,则必须先释放锁,才能继续加锁操作。第三条规则是比较重要的一条规则,也是后面要讲的内容。直观的解释是,如果一个线程先写一个变量,然后一个线程读它,那么写操作肯定会先于读操作发生。第四条规则实际上体现了happens-before原则的传递性。5、深入解析volatile关键字5.1Volatile关键字的两层语义一个共享变量(类的成员变量,类的静态成员变量)一旦被volatile修饰,它有两层语义语义:不同线程保证了变量操作的可见性,即一个线程修改了一个变量的值,这个新值立即对其他线程可见。禁止指令重新排序。关于可见性,先看一段代码。如果线程1先执行,则线程2稍后执行://thread1booleanstop=false;while(!stop){doSomething();}//thread2stop=true;这段代码是很典型的一段代码,很多人在中断线程的时候可能会用到这种标记方式。但实际上,这段代码会完全正确运行吗?即线程会不会被中断?不一定,也许在大多数时候,这段代码可以打断线程,但也有可能导致线程无法被打断(虽然这种可能性很小,但一旦发生就会造成死循环)。下面解释一下为什么这段代码可能会导致线程无法中断。上面解释过,每个线程在运行时都有自己的工作内存,所以线程1在运行时,会复制stop变量的值,放到自己的工作内存中。那么当线程2改变了stop变量的值,但是还没有来得及写入主存,线程2转而去做其他事情,那么线程1就会继续循环,因为它不知道stop的变化线程2的变量下降。但是用volatile修饰后就不一样了:***:使用volatile关键字会强制修改后的值立即写入主存;第二:使用volatile关键字,当线程2修改时,会导致线程1工作内存中缓存变量stop的缓存行无效(反映到硬件层,即L1或L1中对应的缓存行CPU二级缓存失效);第三:因为缓存变量stop在线程1的工作内存中的缓存行是无效的,所以当线程1再次读取变量stop的值时,会去主内存中读取。那么当线程2修改stop值时(当然这包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会导致变量stop的缓存行在线程1的工作内存中缓存的无效,然后线程1读取的时候发现自己的缓存行无效。它会等待cacheline对应的主存地址被更新,然后去对应的主存读取最新的值。然后线程1读到***的正确值。5.2volatile能保证原子性吗?volatile不保证原子性。让我们看一个例子。publicclassTest{publicvolatileintinc=0;publicvoidincrease(){inc++;}publicstaticvoidmain(String[]args){finalTesttest=newTest();for(inti=0;i<10;i++){newThread(){publicvoidrun(){for(intj=0;j<1000;j++)test.increase();};}.start();}while(Thread.activeCount()>1)//保证前面的线程都执行完Thread.yield();System.out.println(test.inc);}}大家想想这个程序的输出是什么?可能有的朋友认为是10000。但是实际上,你运行的时候会发现每次运行的结果都不一致,而且是一个小于10000的数字。这里有一个误区,volatile关键字可以保证可见性是正确的,但是上面的程序在不能保证原子性上是错误的。可见性只能保证每次读取到最新的值,而volatile不能保证对变量操作的原子性。前面说过,自增操作不是原子的,它包括读取变量的原值、加1、写入工作内存。也就是说,自增操作的3个子操作可能会分别执行,这可能会导致如下情况:如果某个时刻变量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变量修改的时候,cacheline会作废?然后其他线程会读取新的值,没错,这是正确的。这就是上面happens-before规则中的volatile变量规则,但是需要注意的是,线程1读取变量后,如果被阻塞,并不会修改inc值。那么虽然volatile可以保证线程2从内存中读取了变量inc的值,但是线程1并没有对其进行修改,所以线程2根本看不到修改后的值。根本原因就在这里,自增操作不是原子操作,volatile不能保证任何对变量的操作都是原子的。将上面的代码改成下面任意一种都可以达到效果:使用synchronized:publicclassTest{publicintinc=0;publicsynchronizedvoidincrease(){inc++;}publicstaticvoidmain(String[]args){finalTesttest=newTest();for(inti=0;i<10;i++){newThread(){publicvoidrun(){for(intj=0;j<1000;j++)test.increase();};}.start();}while(Thread.activeCount()>1)//确保前面的线程都执行完Thread.yield();System.out.println(test.inc);}}使用Lock:publicclassTest{publicintinc=0;Locklock=newReentrantLock();publicvoidincrease(){lock.lock();try{inc++;}finally{lock.unlock();}}publicstaticvoidmain(String[]args){finalTesttest=newTest();for(inti=0;i<10;i++){newThread(){publicvoidrun(){for(intj=0;j<1000;j++)test.increase();};}.start();}while(Thread.activeCount()>1)//保证前面的线程是在执行Thread.yield()之后;System.out.println(test.inc);}}使用AtomicInteger:(inti=0;i<10;i++){newThread(){publicvoidrun(){for(intj=0;j<1000;j++)test.increase();};}.start();}while(Thread.activeCount()>1)//保证previous所有线程都执行了Thread.yield();System.out.println(test.inc);}}java1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即基本数据类型的自动操作Increment(加1操作),自减(减1操作),和加操作(加一个数),减操作(减一个数)进行封装,保证这些操作是原子操作atomic是利用CAS实现原子操作(CompareAndSwap),CAS实际上是利用处理器提供的CMPXCHG指令来实现的,处理器对CMPXCHG指令的执行是一个原子操作。5.3volatile可以保证订单吗?volatile可以在一定程度上保证有序性。volatile关键字禁止指令重排序有两个含义:1)当程序对volatile变量执行读或写操作时,必须对前一次操作进行所有更改,并且结果对后续操作可见;后面的操作一定还没有执行;2)在进行指令优化时,访问volatile变量的语句不能在其后面执行,volatile变量后面的语句也不能在其之前执行。例如://x,y是非易失变量//flag是易失变量x=2;//statement1y=0;//statement2flag=true;//statement3x=4;//statement4y=-1;//Statement5由于flag变量是volatile变量,在对指令重新排序时,statement3不会放在statement1和statement2之前,statement3也不会放在statement4之前和statement5后面。但是需要注意的是,statement1和statement2的顺序,statement4和statement5的顺序是不保证的。而volatile关键字可以保证语句3执行时,语句1和语句2必须执行,语句1和语句2的执行结果对语句3、语句4和语句5可见。5.4原理与实现volatile的机制这里我们讨论volatile如何保证可见性并禁止指令重排序。以下这段话摘自《深入理解Java虚拟机》:《观察加volatile关键字和不加volatile关键字时生成的汇编代码,发现加volatile关键字时,会多出一条锁前缀指令”lockprefix指令其实就相当于一个内存屏障(也称内存屏障),内存屏障会提供3个功能:保证指令重新排序时,后续指令不会被安排到内存之前的位置barrier,也不会把前面的指令安排到后面的内存barrier;也就是说,当到内存屏障的指令执行时,它前面的所有操作都已经完成;它会强制立即将缓存的修改操作写入主存;如果是写操作,会导致其他CPU中对应的缓存行无效。6、使用volatile关键字的场景synchronized关键字是为了防止多个线程同时执行一段代码,这会大大影响程序的执行效率,在某些情况下volatile关键字的性能要优于synchronizedcases,但是要注意volatile关键字不能代替synchronized关键字,因为volatile关键字不能保证操作的原子性。一般来说,volatile的使用必须满足以下两个条件:对变量的写操作不依赖于当前值(如++操作,上面有例子)变量不包含在与其他不变量中variables实际上,这些条件表明可以写入volatile变量的有效值独立于任何程序状态,包括变量的当前状态。其实我的理解是,以上两个条件需要保证操作是原子操作,这样才能保证使用volatile关键字的程序在并发时能够正确执行。下面是Java中使用volatile的几个场景。状态标志volatilebooleanflag=false;while(!flag){doSomething();}publicvoidsetFlag(){flag=true;}volatilebooleaninited=false;//线程1:context=loadContext();inited=true;//线程2:while(!inited){sleep()}doSomethingwithconfig(context);doublecheckclassSingleton{privatevolatilestaticSingletoninstance=null;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance)==null)instance=newSingleton();}}returninstance;}}至于为什么要这样写,请参考:《Java 中的双重检查(Double-Check)》http://blog.csdn.net/dl88250/article/details/5439024和http://www.iteye.com/topic/652440http://www.cnblogs.com/dolphi……
