Java多线程编程那些事:volatile解惑--转
http://www.infoq.com/cn/articles/java-multi-thread-volatile/ 1、 前言volatile关键字可能是Java开发人员“熟悉而又陌生”的一个关键字。本文将从volatile关键字的作用、开销和典型应用场景以及Java虚拟机对volatile关键字的实现这几个方面为读者全面深入剖析volatile关键字。 volatile字面上有“挥发性的,不稳定的”意思,它是用于修饰可变共享变量(Mutable Shared Variable)的一个关键字。所谓“共享”是指一个变量能够被多个线程访问(包括读/写),所谓“可变”是指变量的值可以发生变化。换而言之,volatile关键字用于修饰多个线程并发访问的同一个变量,这些线程中至少有一个线程会更新这个变量的值。我们称volatile修饰的变量为volatile变量。我们知道锁的作用包括保障原子性、保障可见性以及保障有序性。volatile常被称为“轻量级锁”,其作用与锁有类似的地方——volatile也能够保障原子性(仅保障long/double型变量访问操作的原子性)、保障可见性以及保障有序性。 本文所提及的“Java虚拟机”如无特别说明,均特指Oracle公司的HotSpot Java虚拟机。 2. 保障long/double型变量访问操作的原子性不可分割的操作被称为原子操作(Atomic Operation)。所谓不可分割(Indivisible)是指一个操作从其执行线程以外的其他线程看来,该操作要么已经完成要么尚未开始,也就是说其他线程不会看到该操作的中间结果。如果一个操作是原子操作,那么我们就称该操作具有原子性(Atomicity)。 Java语言规范(Java Language Specification,JLS)规定,Java语言中针对long/double型以外的任何变量(包括基础类型变量和引用型变量)进行的读、写操作都是原子操作,即Java语言规范本身并不规定针对long/double型变量进行读、写操作具有原子性。一个long/double型变量的读/写操作在32位Java虚拟机下可能会被分解为两个子步骤(比如先写低32位,再写高32位)来实现,这就导致一个线程对long/double型变量进行的写操作的中间结果可以被其他线程所观察到,即此时针对long/double型变量的访问操作不是原子操作。清单1所示的实验展示了这点。 清单1 long/double型变量写操作的原子性问题Demo 本Demo必须使用32位Java虚拟机才能看到非原子操作的效果. 运行本Demo时也可以指定虚拟机参数“-client” @author Viscent Huang */ <span class="token keyword">public <span class="token keyword">class <span class="token class-name">NonAtomicAssignmentDemo <span class="token keyword">implements <span class="token class-name">Runnable <span class="token punctuation">{ <span class="token keyword">static <span class="token keyword">long value <span class="token operator">= <span class="token number">0<span class="token punctuation">; <span class="token keyword">private <span class="token keyword">final <span class="token keyword">long valueToSet<span class="token punctuation">; <span class="token keyword">public <span class="token function">NonAtomicAssignmentDemo<span class="token punctuation">(<span class="token keyword">long valueToSet<span class="token punctuation">) <span class="token punctuation">{ <span class="token keyword">this<span class="token punctuation">.valueToSet <span class="token operator">= valueToSet<span class="token punctuation">; <span class="token punctuation">} <span class="token keyword">public <span class="token keyword">static <span class="token keyword">void <span class="token function">main<span class="token punctuation">(String<span class="token punctuation">[<span class="token punctuation">] args<span class="token punctuation">) <span class="token punctuation">{ <span class="token comment">// 线程updateThread1将data更新为0 Thread updateThread1 <span class="token operator">= <span class="token keyword">new <span class="token class-name">Thread<span class="token punctuation">(<span class="token keyword">new <span class="token class-name">NonAtomicAssignmentDemo<span class="token punctuation">(0L<span class="token punctuation">)<span class="token punctuation">)<span class="token punctuation">; <span class="token comment">// 线程updateThread2将data更新为-1 Thread updateThread2 <span class="token operator">= <span class="token keyword">new <span class="token class-name">Thread<span class="token punctuation">(<span class="token keyword">new <span class="token class-name">NonAtomicAssignmentDemo<span class="token punctuation">(<span class="token operator">-1L<span class="token punctuation">)<span class="token punctuation">)<span class="token punctuation">; updateThread1<span class="token punctuation">.<span class="token function">start<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">; updateThread2<span class="token punctuation">.<span class="token function">start<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">; <span class="token comment">// 不进行实际输出的OutputStream <span class="token keyword">final DummyOutputStream dos <span class="token operator">= <span class="token keyword">new <span class="token class-name">DummyOutputStream<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">; <span class="token keyword">try <span class="token punctuation">(PrintStream dummyPrintSteam <span class="token operator">= <span class="token keyword">new <span class="token class-name">PrintStream<span class="token punctuation">(dos<span class="token punctuation">)<span class="token punctuation">;<span class="token punctuation">) <span class="token punctuation">{ <span class="token comment">// 共享变量value的快照(即瞬间值) <span class="token keyword">long snapshot<span class="token punctuation">; <span class="token keyword">while <span class="token punctuation">(<span class="token number">0 <span class="token operator">== <span class="token punctuation">(snapshot <span class="token operator">= value<span class="token punctuation">) <span class="token operator">|| <span class="token operator">-<span class="token number">1 <span class="token operator">== snapshot<span class="token punctuation">) <span class="token punctuation">{ <span class="token comment">// 不进行实际的输出,仅仅是为了阻止JIT编译器做循环不变表达式外提优化 dummyPrintSteam<span class="token punctuation">.<span class="token function">print<span class="token punctuation">(snapshot<span class="token punctuation">)<span class="token punctuation">; <span class="token punctuation">} System<span class="token punctuation">.err<span class="token punctuation">.<span class="token function">printf<span class="token punctuation">(<span class="token string">"Unexpected data: %d(0x%016x)"<span class="token punctuation">,snapshot<span class="token punctuation">,snapshot<span class="token punctuation">)<span class="token punctuation">; <span class="token punctuation">} System<span class="token punctuation">.<span class="token function">exit<span class="token punctuation">(<span class="token number">0<span class="token punctuation">)<span class="token punctuation">; <span class="token punctuation">} <span class="token keyword">static <span class="token keyword">class <span class="token class-name">DummyOutputStream <span class="token keyword">extends <span class="token class-name">OutputStream <span class="token punctuation">{ @Override <span class="token keyword">public <span class="token keyword">void <span class="token function">write<span class="token punctuation">(<span class="token keyword">int b<span class="token punctuation">) <span class="token keyword">throws IOException <span class="token punctuation">{ <span class="token comment">// 不实际进行输出 <span class="token punctuation">} <span class="token punctuation">} @Override <span class="token keyword">public <span class="token keyword">void <span class="token function">run<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{ <span class="token keyword">for <span class="token punctuation">(<span class="token punctuation">;<span class="token punctuation">;<span class="token punctuation">) <span class="token punctuation">{ value <span class="token operator">= valueToSet<span class="token punctuation">; <span class="token punctuation">} <span class="token punctuation">} <span class="token punctuation">} 使用32位(而不是64位)Java虚拟机运行清单1所示的Demo我们可以看到该程序的输出是:
或者,
可见,main线程读取到共享变量value的值可能既不是0(对应无符号16进制数0x0000000000000000)也不是-1(对应无符号16进制数0xffffffffffffffff),而是其他两个线程更新value时的“中间结果”——4294967295(对应无符号16进制数0x00000000ffffffff)或者-4294967296(对应无符号16进制数0xffffffff00000000),即一个线程对value变量的低(Lower)32位(4个字节)更新与另外一个线程对value变量的高(Higher)32位(4个字节)更新所“混合”出来的一个非预期的错误结果。因此,上述Demo对共享变量value的写操作并非一个原子操作。这是由于:Java平台中,long/double型变量会占用64位(8个字节)的存储空间,而32位的Java虚拟机对这种变量的写操作可能会被分解为两个子步骤来实施,比如先写低32位,再写高32位。那么,多个线程试图共享同一个这样的变量时就可能出现一个线程在写高32位的时候,另外一个线程恰好正在写低32位,而此刻第三个线程读取这个变量时所读取到的变量值仅仅是其他两个线程更新这个变量的中间结果。 32位虚拟机下,一个long/double型变量读操作同样也可能会被分解为两个子步骤来实现,比如先读取低32位到寄存器中,再读取高32位到寄存器中。这种实现同样也会导致与上述Demo所展示的相似的效果,即一个线程可以读取到其他线程对long/double型变量写操作的中间结果。因此,在这种Java虚拟机实现下,long/double型变量读操作同样也不是原子操作。 上述Demo更多的是从系统(Java虚拟机)层面展示原子性问题。那么,在业务层面我们是否也可能遇到类似上述的原子性问题呢?如清单2所示,假设线程T1通过执行updateHostInfo方法来更新主机信息(HostInfo),线程T2则通过执行connectToHost方法来读取主机信息,并据此与相应的主机建立网络连接。那么,updateHostInfo方法中的操作(更新主机IP地址和端口号)必须是一个原子操作,即这个操作必须是“不可分割”的。否则,可能出现这样的情形:假设hostInfo的初始值表示的是IP地址为“192.168.1.101”、端口号为8081的主机,T1执行updateHostInfo方法试图将hostInfo更新为IP地址为“192.168.1.100”、端口号为8080的主机的时候,T2可能刚好执行connectToHost方法,那么此时由于T1可能刚刚执行完语句①而未开始语句②(即只更新完IP地址而尚未更新端口号),因此T2可能读取到IP地址为“192.168.1.100”、而端口号却仍然为8081的主机信息,即T2读取到了一个错误的主机信息(IP地址为“192.168.1.100”的主机上面并没有开启侦听端口8081,它开启的8080)从而无法建立网络连接!这里的错误是由于updateHostInfo方法中的操作不是原子操作(不具备“不可分割”的特性)而使其他线程读取了脏数据(错误的主机信息)导致的。 清单2 业务层面的原子操作问题Demo<span class="token keyword">private HostInfo hostInfo<span class="token punctuation">; 当然,上述原子性问题都可以通过加锁解决。不过,Java语言规范特别地规定针对volatile修饰的long/double型变量进行的读、写操作也具有原子性。换而言之,volatile关键字能够保障long/double型变量访问操作的原子性。需要注意的是,volatile对原子性的保障仅限于共享变量写和读操作本身。对共享变量进行的赋值操作实际上往往是一个复合操作,volatile并不能保障这些赋值操作的原子性。例如,如下针对volatile变量counter1赋值语句: 如果counter2是一个局部变量,那么上述赋值语句实际上就是针对counter1的写操作,因此在volatile关键字的作用下上述赋值操作具有原子性。如果counter2也是一个共享变量,那么上述赋值语句就不具有原子性。这是由于此时上述语句实际上可以被分解为如下几个子操作(伪代码表示): r1 <span class="token operator">= r1 <span class="token operator">+ <span class="token number">1<span class="token punctuation">;<span class="token comment">//子操作②:将寄存器r1的值增加1 volatile关键字并不像锁那样具有排他性,在写操作方面,其对原子性的保障也仅仅作用于上述的子操作③(变量写操作)。因此,一个线程在执行到子操作③的时候,其他线程可能已经更新了共享变量counter2的值,这就使得子操作③的执行线程实际上是向共享变量counter1写入了一个旧值。 因此,对volatile变量的赋值操作其表达式右边不能包含任何共享变量(包括被赋值的volatile变量本身)。 依照Java语言规范,对volatile修饰的long/double型变量的进行的读操作也具有原子性。因此,我们说volatile能够保障long/double型变量访问操作的原子性。 3. 保障可见性可见性(Visibility)是指一个线程(读线程)是否或者在什么情况下能够读取到其他线程(写线程)对共享变量所做的更新。由于软件、硬件的原因,一个线程(写线程)对共享变量进行更新之后,其他线程(读线程)再来读取该变量的时候,这些读线程可能无法读取到写线程对共享变量所做的更新,清单3展示了这点。 清单3 可见性问题Demo<span class="token keyword">public <span class="token keyword">static <span class="token keyword">void <span class="token function">main<span class="token punctuation">(String<span class="token punctuation">[<span class="token punctuation">] args<span class="token punctuation">) <span class="token keyword">throws InterruptedException <span class="token punctuation">{ 该Demo中,我们为子线程backgroundThread(类型为CountingThread)设置了一个停止标记ready。当ready值为true时,子线程通过使其run方法返回而实现线程的终止。然而,使用Java虚拟机的server模式运行上述Demo,我们可以发现该Demo中的子线程并没有像我们预期的那样在1秒钟之后终止而是一直在运行!由此可见,主线程(main线程)对共享变量ready所做的更新(将ready设置为true)并没有被子线程backgroundThread所读取到。究其原因,这是HotSpot虚拟机的C2编译器(Just In Time编译器)在将字节码动态编译为本地机器码的过程中执行循环不变量外提( Loop-invariant code motion)优化的结果:由于该Demo中的共享变量ready并没有采用volatile修饰,因此C2编译器会认为该变量并不会被多个线程访问(实际上有多个线程访问该变量),于是C2编译器为了提升代码执行效率而将CountingThread.run()中的while循环语句优化为与如下伪代码等效的机器码: <span class="token keyword">while<span class="token punctuation">(<span class="token boolean">true<span class="token punctuation">)<span class="token punctuation">{ 这种优化可以通过查看C2编译器所产生的汇编代码来确认,如图1所示。不幸的是,这种优化导致了死循环! 图1 C2编译器循环不变量外提优化所产生的汇编代码如果我们采用volatile修饰上述Demo中的ready变量,那么C2编译器便会“意识”到ready是一个共享变量,因此就不会对CountingThread.run()中的while循环语句执行循环不变量外提优化从而避免了死循环。 当然,硬件的因素也可能导致可见性问题。处理器为了提高内存写操作的效率而引入的硬件部件写缓冲器(Store Buffer)和无效化队列(Invalidate Queue)都可能导致一个线程对共享变量所做的更新无法被后续线程所读取到。 Java语言规范规定,对于同一个volatile变量,一个线程(写线程)对该变量进行更新,其他线程(读线程)随后对该变量进行读取,这些线程总是可以读取到写线程对该变量所做的更新。换而言之,写线程更新一个volatile变量,读线程随后来读取该变量,那么这些读线程能够读取到写线程对该变量所做的更新这一点是有保障的(而不是碰运气!)。不过,由于volatile并不具有锁那样的排他性,因此volatile并不能够保障读线程所读取到变量值是共享变量的最新值:读线程在读取一个volatile变量的那一刻,其他线程(写线程)可能又恰好更新了该变量,因此读线程所读取到共享变量值仅仅是一个相对新值,即其他线程更新过的值(不一定是最新值)。 4. 小结以上我们介绍了volatile关键字对long/double型变量访问操作的原子性保障以及对可见性的保障。接下来我们将介绍volatile对有序性的保障,并通过介绍Java内存模型中的Happens-before关系这一概念来深入理解volatile对可见性和有序性的保障。 5. 保障有序性一个处理器上的线程所执行的一组操作在其他处理器上的线程看来可能是乱序的(Out-of-order),即这些线程对这组操作中的各个操作的感知顺序(观察到的顺序)与程序顺序(目标代码中指定的顺序)不一致。 下面我们看一个乱序实验,如清单4所示。 清单4 JIT编译器指令重排序Demo再现JIT指令重排序的Demo @author Viscent Huang */ @<span class="token function">ConcurrencyTest<span class="token punctuation">(iterations <span class="token operator">= <span class="token number">200000<span class="token punctuation">) <span class="token keyword">public <span class="token keyword">class <span class="token class-name">JITReorderingDemo <span class="token punctuation">{ <span class="token keyword">private <span class="token keyword">int externalData <span class="token operator">= <span class="token number">1<span class="token punctuation">; <span class="token keyword">private Helper helper<span class="token punctuation">; @Actor <span class="token keyword">public <span class="token keyword">void <span class="token function">createHelper<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{ helper <span class="token operator">= <span class="token keyword">new <span class="token class-name">Helper<span class="token punctuation">(externalData<span class="token punctuation">)<span class="token punctuation">; <span class="token punctuation">} @<span class="token function">Observer<span class="token punctuation">(<span class="token punctuation">{ @<span class="token function">Expect<span class="token punctuation">(desc <span class="token operator">= <span class="token string">"Helper is null"<span class="token punctuation">,expected <span class="token operator">= <span class="token operator">-<span class="token number">1<span class="token punctuation">)<span class="token punctuation">,@<span class="token function">Expect<span class="token punctuation">(desc <span class="token operator">= <span class="token string">"Helper is not null,but it is not initialized"<span class="token punctuation">,expected <span class="token operator">= <span class="token number">0<span class="token punctuation">)<span class="token punctuation">,@<span class="token function">Expect<span class="token punctuation">(desc <span class="token operator">= <span class="token string">"Only 1 field of Helper instance was initialized"<span class="token punctuation">,expected <span class="token operator">= <span class="token number">1<span class="token punctuation">)<span class="token punctuation">,@<span class="token function">Expect<span class="token punctuation">(desc <span class="token operator">= <span class="token string">"Only 2 fields of Helper instance were initialized"<span class="token punctuation">,expected <span class="token operator">= <span class="token number">2<span class="token punctuation">)<span class="token punctuation">,@<span class="token function">Expect<span class="token punctuation">(desc <span class="token operator">= <span class="token string">"Only 3 fields of Helper instance were initialized"<span class="token punctuation">,expected <span class="token operator">= <span class="token number">3<span class="token punctuation">)<span class="token punctuation">,@<span class="token function">Expect<span class="token punctuation">(desc <span class="token operator">= <span class="token string">"Helper instance was fully initialized"<span class="token punctuation">,expected <span class="token operator">= <span class="token number">4<span class="token punctuation">) <span class="token punctuation">}<span class="token punctuation">) <span class="token keyword">public <span class="token keyword">int <span class="token function">consume<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{ <span class="token keyword">int sum <span class="token operator">= <span class="token number">0<span class="token punctuation">; <span class="token comment">/*
*/ <span class="token keyword">final Helper observedHelper <span class="token operator">= helper<span class="token punctuation">; <span class="token keyword">if <span class="token punctuation">(null <span class="token operator">== observedHelper<span class="token punctuation">) <span class="token punctuation">{ sum <span class="token operator">= <span class="token operator">-<span class="token number">1<span class="token punctuation">; <span class="token punctuation">} <span class="token keyword">else <span class="token punctuation">{ sum <span class="token operator">= observedHelper<span class="token punctuation">.payloadA <span class="token operator">+ observedHelper<span class="token punctuation">.payloadB <span class="token operator">+ observedHelper<span class="token punctuation">.payloadC <span class="token operator">+ observedHelper<span class="token punctuation">.payloadD<span class="token punctuation">; <span class="token punctuation">} <span class="token keyword">return sum<span class="token punctuation">; <span class="token punctuation">} <span class="token keyword">static <span class="token keyword">class <span class="token class-name">Helper <span class="token punctuation">{ <span class="token keyword">int payloadA<span class="token punctuation">; <span class="token keyword">int payloadB<span class="token punctuation">; <span class="token keyword">int payloadC<span class="token punctuation">; <span class="token keyword">int payloadD<span class="token punctuation">; <span class="token keyword">public <span class="token function">Helper<span class="token punctuation">(<span class="token keyword">int externalData<span class="token punctuation">) <span class="token punctuation">{ <span class="token keyword">this<span class="token punctuation">.payloadA <span class="token operator">= externalData<span class="token punctuation">; <span class="token keyword">this<span class="token punctuation">.payloadB <span class="token operator">= externalData<span class="token punctuation">; <span class="token keyword">this<span class="token punctuation">.payloadC <span class="token operator">= externalData<span class="token punctuation">; <span class="token keyword">this<span class="token punctuation">.payloadD <span class="token operator">= externalData<span class="token punctuation">; <span class="token punctuation">} <span class="token punctuation">} <span class="token keyword">public <span class="token keyword">static <span class="token keyword">void <span class="token function">main<span class="token punctuation">(String<span class="token punctuation">[<span class="token punctuation">] args<span class="token punctuation">) <span class="token keyword">throws InstantiationException<span class="token punctuation">,IllegalAccessException <span class="token punctuation">{ <span class="token comment">// 调用测试工具运行测试代码 TestRunner<span class="token punctuation">.<span class="token function">runTest<span class="token punctuation">(JITReorderingDemo<span class="token punctuation">.<span class="token keyword">class<span class="token punctuation">)<span class="token punctuation">; <span class="token punctuation">} <span class="token punctuation">} 清单4中的程序非常简单(读者可以忽略其中的注解,因为那是给测试工具用的):createHelper方法会将实例变量helper更新为一个新创建的Helper实例;consume方法会读取helper所引用的Helper实例,并计算该实例的所有字段(payloadA~payloadD)的值之和作为其返回值。该程序的main方法调用测试工具TestRunner的runTest方法的作用是让测试工具安排一些线程并发地执行createHelper方法和consume方法,并统计consume方法多次执行的返回值。由于createHelper方法创建Helper实例的时候使用的构造器参数externalData值为1,因此这样看来consume方法的返回值似乎“理所当然”地应该是4。然而,事实却并不总是如此。使用如下命令以server模式并设置Java虚拟机参数“-XX:-UseCompressedOops”运行清单4所示的程序[1]:
我们可以看到类似如下的输出[2]:
上面的输出中,expected后面的数字表示consume方法的返回值,相应的occurrences表示出现相应返回值的次数。 不难看出这次程序运行时,有几次consume方法的返回值并不为4:有的为3(出现4次)、有的为2(出现1次),甚至还有的为0(出现2次)!这说明consume方法的执行线程有时候读取到了一个未初始化完毕(或者正在初始化)的Helper实例:Helper实例不为null,但是其部分实例字段的字段值仍然为其默认值而非Helper类的构造器中指定的初始值。下面我们分析其中的原因。 我们知道,createHelper方法中的唯一一条语句:
可以分解为以下几个子操作(伪代码表示):
通过查看Java字节码不难发现createHelper方法中指定的程序顺序就是上述的先初始化Helper实例(子操作②)再将相应的实例的引用赋值给实例变量helper(子操作③)。然而,consume方法的执行线程却观察到了未初始化完毕的Helper实例,这说明该线程对createHelper方法所执行的操作的感知顺序与该方法所指定的程序顺序不一致,即产生了乱序。 查看上述程序运行过程中JIT编译器动态生成的汇编代码(相当于机器码),如图2所示,我们可以发现JIT编译器编译字节码的时候并不是每次都按照上述源代码顺序(这里同时也是程序顺序)生成相应的机器码(汇编代码):JIT编译器将子操作③相应的指令重排到子操作②相应的指令之前,即JIT编译器在初始化Helper实例之前可能已经将对该实例的引用写入helper实例变量。这就导致了其他线程(consume方法的执行线程)看到helper实例变量(不为null)的时候,该实例变量所引用的对象可能还没有被初始化或者未初始化完毕(即相应构造器中的代码未执行结束)。这就解释了为什么我们在运行上述程序的时候,consume方法的返回值有时候并不是4。 图2?JIT编译器重排序Demo中的汇编代码片段虽然乱序有利于充分发挥处理器的指令执行效率,但是正如上述实验所展示的,它也可能导致程序正确性的问题。所以,为了保障程序的正确性,有时候我们需要确保线程对一组操作的感知顺序与这组操作的程序顺序保持一致,即保障这组操作的有序性。上述实验中,为了确保consume方法的执行线程看到的Helper实例总是初始化完毕的,我们需要确保createHelper方法所执行的操作的有序性。为此,我们只需要用volatile关键字来修饰实例变量helper即可,而无需借助锁。这里,volatile关键字所起的作用是通过禁止子操作②被JIT编译器以及处理器重排序(指令重排序、内存重排序)到子操作③之后,从而保障了有序性。 Java语言规范规定,对于访问(读、写)同一个volatile变量的多个线程而言,一个线程(写线程)在写volatile变量前所执行的内存读、写操作在随后读取该volatile变量的其他线程(读线程)看来是有序的。设X、Y是普通(非volatile)共享变量,其初始值均为0,V是volatile变量,其初始值为false,r1、r2是局部变量,线程T1和T2先后访问V,如图3所示。那么,T1对V的更新以及更新V前所执行的操作在T2看来是有序的:在T2看来T1对X、Y和V的写操作就像是完全依照程序顺序执行的。换而言之,如果T2读取到V的值为true,那么该线程所读取到的X和Y的值必然为分别为1和2。相反,如果V不是volatile变量,那么上述这种保证就不存在,即T2读取到V的值为true时,T2所读取到X和Y的值可能并非1和2。 图3 volatile关键字的有序性保障示例代码上述例子中,我们假设只有一个线程更新V(另外一个线程读取V),如果有更多的线程并发更新V,那么由于volatile并不具有排他性,因此在T2读取V的时候T1之外的其他线程可能已经又更新了共享变量X、Y,这就使得T2在其读取到V的值为true的情况下,其读取到X和Y的值可能不是1和2。不过,这种现象是数据竞争的结果,这与volatile能够保障有序性本身并不矛盾。 6. Happens-before关系了解Java内存模型(Java Memory Model)中的定义的Happens-before关系(Happens-before Relationship)这一概念有助于我们进一步理解volatile变量对可见性和有序性的保障。 Java内存模型定义了一些动作(Action)。这些动作包括变量的读/写、锁的申请(lock)与释放(unlock)以及线程的启动(Thread.start()调用)和加入(Thread.join()调用)等。如果动作A和动作B之间存在Happens-before关系,那么动作A的执行结果对动作B可见。反之,如果动作A和动作B之间不存在Happens-before关系,那么动作A的执行结果对B来说不一定是可见的。下文我们用“→”来表示Happens-before关系,例如“A→B”表示动作A与动作B存在Happens-before关系。 Java内存模型中的volatile变量规则(Volatile Variable Rule)规定,对一个volatile变量的写操作happens-before后续(Subsequent)每一个针对该变量的读操作。这里有两点需要注意:首先,针对同一个volatile变量的写、读操作之间才有happens-before关系,不同volatile变量之间的写、读操作并无happens-before关系;其次,针对同一个volatile变量的写、读操作必须具有时间上的先后关系,即一个线程先写另外一个线程再来读这样这两个动作之间才能够有happens-before关系。因此,对于图2可有wV→rV,即动作wV(写volatile变量V)的结果对rV(读volatile变量V)可见。 Java内存模型中程序顺序规则(Program Order Rule)规定同一个线程中的每一个动作都happens-before该线程中程序顺序上排在该动作之后的每一个动作。因此,对于图3可有如下的happens-before关系:
Happens-before关系具有传递性,即如果A→B,B→C,那么就有A→C。因此,由hb1和hb2可得出以下happens-before关系:
再根据volatile变量规则,可有happens-before关系:
进一步根据happens-before关系的传递性由hb5和hb6可得出以下happens-before关系:
同样根据happens-before关系的传递性由hb7和hb3可得出以下happens-before关系:
同理,我们也可以推断出以下happens-before关系:
由此可见,线程T1对普通共享变量X和Y所做的更新对线程T2来说都是可见的。这种可见性是在volatile变量规则、程序顺序规则以及happens-before关系的传递性的共同作用下得以保障的。因此,我们说volatile关键字不仅仅保障写线程对volatile变量所做的更新的可见性(hb6),它还保障了写线程在写volatile变量前对其他非volatile变量所做的更新的可见性(hb8和hb9)。 理解了Happens-before关系这一概念之后,我们可以思考这样一个问题:volatile关键字对可见性和有序性的保障是否适用于数组呢?例如,对于volatile修饰的一个int数组vArr,线程A执行“vArr[0]=1;”,接着,线程B再来读取vArr的第1个元素,那么此时线程B所读取到元素值是否一定是“1”呢(这里我们假设只有线程A和线程B这两个线程访问vArr)?答案是“不一定”:此时线程A和线程B从volatile关键字的角度来看都只是读线程(读取volatile变量vArr),即这两个线程之间并不存在Happens-before关系,因此线程A对vArr第1个元素的更新对线程B来说不一定是可见的。这个例子中,要保障对数组元素的更新的可见性,我们可以使用java.util.concurrent.atomic.AtomicIntegerArray类。 7. 小结上面介绍了volatile对有序性的保障,并通过介绍Java内存模型中的Happens-before关系这一概念来进一步介绍volatile对可见性和有序性的保障。通过前面的介绍,我们知道volatile关键字的作用包括保障long/double型变量访问操作的原子性、保障可见性和保障有序性。接下来将介绍Java虚拟机对volatile关键字的实现,volatile关键字的开销以及volatile的典型应用场景。 8. Java虚拟机对volatile的实现本节会涉及较多的术语,如表1所示。 表1 本节术语 |