Java multithreaded programming: volatile solution — turn

http://www.infoq.com/cn/articles/java-multi-thread-volatile/

1. Foreword

The volatile keyword may be a keyword that Java developers are "familiar and unfamiliar". This paper will comprehensively and deeply analyze volatile keyword for readers from the aspects of its function, cost, typical application scenarios and the implementation of volatile keyword by Java virtual machine.

Volatile literally means "volatile, unstable", It is a keyword used to modify mutable shared variable. The so-called "shared" refers to that a variable can be accessed by multiple threads (including read / write), and the so-called "variable" It means that the value of a variable can change. In other words, the volatile keyword is used to modify the same variable accessed concurrently by multiple threads. At least one of these threads will update the value of this variable. We call volatile modified variables volatile variables. We know that the role of lock includes ensuring atomicity, visibility and order. Volatile is often referred to as "lightweight lock". Its role is similar to that of lock - volatile can also guarantee atomicity (only the atomicity of long / double variable access operations), visibility and order.

The "Java virtual machine" mentioned in this article refers to the Oracle hotspot Java virtual machine unless otherwise specified.

2. Ensure the atomicity of long / double variable access operation

An indivisible operation is called an atomic operation (independent) refers to an operation. From the perspective of other threads other than its execution thread, the operation has either completed or not started, that is, other threads will not see the intermediate results of the operation. If an operation is atomic, we call it atomic.

The Java language specification (JLS) stipulates that any variable other than long / double type in the Java language The read and write operations (including basic type variables and reference type variables) are atomic operations, that is, the Java language specification itself does not specify that the read and write operations for long / double variables are atomic. The read / write operation of a long / double variable may be divided into two sub steps under the 32-bit Java virtual machine (for example, write the lower 32 bits first, and then write the higher 32 bits). This leads to the intermediate result of a thread's write operation on a long / double variable that can be observed by other threads, that is, the access operation on a long / double variable at this hour is not an atomic operation. The experiment shown in Listing 1 shows this.

Listing 1 demo of atomicity problem of long / double variable write operation

  • 本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">}

    Run the demo shown in Listing 1 with a 32-bit (instead of 64 bit) Java virtual machine. We can see that the output of the program is:

    Unexpected data: 4294967295(0x00000000ffffffff)

    Or,

    Unexpected data: -4294967296(0xffffffff00000000)

    so The value of the shared variable value read by the main thread may be neither 0 (corresponding to the unsigned hexadecimal number 0x0000000000000000000000) nor - 1 (corresponding to the unsigned hexadecimal number 0xffffffffffff), but the "intermediate result" when the other two threads update value - 4294967295 (corresponding to the unsigned hexadecimal number 0x00000000ffffff) or - 4294967296 (corresponding to the unsigned hexadecimal number 0xffffffff00000000), that is, one thread updates the low 32 bits (4 bytes) of the value variable and another thread updates the high 32 bits (4 bytes) of the value variable An unexpected error result. Therefore, the write operation of the above demo to the shared variable value is not an atomic operation. This is because: in the Java platform, Long / double variables occupy 64 bits (8 bytes) of storage space, and the 32-bit Java virtual machine's write operation on this variable may be divided into two sub steps. For example, write the lower 32 bits first, and then write the higher 32 bits. Then, when multiple threads try to share the same variable, one thread may write the higher 32 bits while the other thread is writing the lower 32 bits, and the third one at the moment The variable value read by the thread when reading this variable is only the intermediate result of the other two threads updating this variable.

    In a 32-bit virtual machine, a long / double variable reading operation may also be divided into two sub steps. For example, read the low 32 bits into the register first, and then read the high 32 bits into the register. This implementation will also lead to an effect similar to that shown in the above demo, that is, one thread can read the intermediate results of other threads' write operations on long / double variables. Therefore, in this Java virtual machine implementation, the long / double variable reading operation is also not an atomic operation.

    The above demo mainly shows the atomicity problem from the system (Java virtual machine) level. Then, at the business level, may we encounter the atomicity problem similar to the above? As shown in Listing 2, suppose that thread T1 updates the host information by executing the updatehostinfo method (hostinfo), thread T2 reads the host information by executing the connecttohost method and establishes a network connection with the corresponding host. Then, the operation (updating the host IP address and port number) in the updatehostinfo method must be an atomic operation, that is, the operation must be "indivisible" of Otherwise, it may occur that the initial value of hostinfo represents a host with IP address "192.168.1.101" and port number 8081. When T1 executes the updatehostinfo method to update hostinfo to a host with IP address "192.168.1.100" and port number 8080, T2 may just execute the connecttohost method, At this time, T1 may have just executed statement ① but not started statement ② (that is, only the IP address has been updated but the port number has not been updated), so T2 may read the host information with the IP address "192.168.1.100" and the port number is still 8081, that is, T2 reads an incorrect host information (the IP address is "192.168.1.100") The listening port 8081 is not opened on the host, so the network connection cannot be established! The error here is caused by other threads reading dirty data (wrong host information) because the operation in the updatehostinfo method is not an atomic operation (does not have the "indivisible" feature).

    Listing 2 demo of atomic operation problem at business level
    <span class="token keyword">private HostInfo hostInfo<span class="token punctuation">;

    <span class="token keyword">public <span class="token keyword">void <span class="token function">updateHostInfo<span class="token punctuation">(String ip<span class="token punctuation">,<span class="token keyword">int port<span class="token punctuation">) <span class="token punctuation">{

    <span class="token comment">// 以下操作不是原子操作

    hostInfo<span class="token punctuation">.<span class="token function">setIp<span class="token punctuation">(ip<span class="token punctuation">)<span class="token punctuation">;<span class="token comment">// 语句①

    hostInfo<span class="token punctuation">.<span class="token function">setPort<span class="token punctuation">(port<span class="token punctuation">)<span class="token punctuation">;<span class="token comment">// 语句②

    <span class="token punctuation">}

    <span class="token keyword">public <span class="token keyword">void <span class="token function">connectToHost<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{

    String ip <span class="token operator">= hostInfo<span class="token punctuation">.<span class="token function">getIp<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;

    <span class="token keyword">int port <span class="token operator">= hostInfo<span class="token punctuation">.<span class="token function">getPort<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;

    <span class="token function">connectToHost<span class="token punctuation">(ip<span class="token punctuation">,port<span class="token punctuation">)<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token keyword">private <span class="token keyword">void <span class="token function">connectToHost<span class="token punctuation">(String ip<span class="token punctuation">,<span class="token keyword">int port<span class="token punctuation">) <span class="token punctuation">{

    <span class="token comment">// ...

    <span class="token punctuation">}

    <span class="token keyword">public <span class="token keyword">static <span class="token keyword">class <span class="token class-name">HostInfo <span class="token punctuation">{

    <span class="token keyword">private String ip<span class="token punctuation">;

    <span class="token keyword">private <span class="token keyword">int port<span class="token punctuation">;

    <span class="token keyword">public <span class="token function">HostInfo<span class="token punctuation">(String ip<span class="token punctuation">,<span class="token keyword">int port<span class="token punctuation">) <span class="token punctuation">{

    <span class="token keyword">this<span class="token punctuation">.ip <span class="token operator">= ip<span class="token punctuation">;

    <span class="token keyword">this<span class="token punctuation">.port <span class="token operator">= port<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token comment">//...

    <span class="token punctuation">}

    <span class="token punctuation">}

    Of course, the above atomic problems can be solved by locking. However, the Java language specification specifically stipulates that the read and write operations for long / double variables modified by volatile are also atomic. In other words, the volatile keyword can guarantee the atomicity of long / double variable access operations. It should be noted that volatile's guarantee of atomicity is limited to the shared variable write and read operations themselves. The assignment of shared variables is often a composite operation, and volatile cannot guarantee the atomicity of these assignment operations. For example, the following assignment statement for volatile variable counter1:

    If counter2 is a local variable, the above assignment statement is actually a write operation for counter1. Therefore, under the action of volatile keyword, the above assignment operation is atomic. If counter2 is also a shared variable, the above assignment statement is not atomic. This is because the above statement can actually be decomposed into the following sub operations (pseudo code representation):

    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

    counter1 <span class="token operator">= r1<span class="token punctuation">;<span class="token comment">//子操作③:将寄存器r1的值写入共享变量counter1(内存)

    Volatile keyword is not as exclusive as lock. In terms of write operation, Its atomicity guarantee only applies to the above sub operation ③ (variable write operation). Therefore, when a thread executes the sub operation ③, other threads may have updated the value of the shared variable counter2, so that the execution thread of the sub operation ③ actually writes an old value to the shared variable counter1.

    Therefore, the assignment of volatile variables cannot contain any shared variables (including the assigned volatile variable itself) on the right side of the expression.

    According to the Java language specification, the reading operation of long / double variables modified by volatile is also atomic. Therefore, we say that volatile can guarantee the atomicity of long / double variable access operations.

    3. Ensure visibility

    Visibility refers to whether or under what circumstances a thread (read thread) can read the updates of shared variables made by other threads (write threads). Due to software and hardware reasons, after a thread (write thread) updates shared variables, other threads (read thread) when reading the variable again, these read threads may not be able to read the updates made by the write thread to the shared variable. Listing 3 shows this.

    Listing 3: visibility issue 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">{

    CountingThread backgroundThread <span class="token operator">= <span class="token keyword">new <span class="token class-name">CountingThread<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;

    backgroundThread<span class="token punctuation">.<span class="token function">start<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;

    Thread<span class="token punctuation">.<span class="token function">sleep<span class="token punctuation">(<span class="token number">1000<span class="token punctuation">)<span class="token punctuation">;

    backgroundThread<span class="token punctuation">.<span class="token function">cancel<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;

    backgroundThread<span class="token punctuation">.<span class="token function">join<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;

    System<span class="token punctuation">.out<span class="token punctuation">.<span class="token function">printf<span class="token punctuation">(<span class="token string">"count:%s"<span class="token punctuation">,backgroundThread<span class="token punctuation">.count<span class="token punctuation">)<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token punctuation">}

    <span class="token keyword">class <span class="token class-name">CountingThread <span class="token keyword">extends <span class="token class-name">Thread <span class="token punctuation">{

    <span class="token comment">//线程停止标志

    <span class="token keyword">private <span class="token keyword">boolean ready <span class="token operator">= <span class="token boolean">false<span class="token punctuation">;

    <span class="token keyword">public <span class="token keyword">int count <span class="token operator">= <span class="token number">0<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">while <span class="token punctuation">(<span class="token operator">!ready<span class="token punctuation">) <span class="token punctuation">{

    count<span class="token operator">++<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token punctuation">}

    <span class="token keyword">public <span class="token keyword">void <span class="token function">cancel<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{

    ready <span class="token operator">= <span class="token boolean">true<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token punctuation">}

    In this demo, We create a backgroundthread for the child thread (type: countingthread) sets a stop flag ready. When the ready value is true, the child thread terminates the thread by returning its run method. However, running the above demo in the server mode of the Java virtual machine, we can find that the child thread in the demo does not terminate after 1 second as we expected, but runs all the time! This shows that, The update made by the main thread to the shared variable ready (set ready to true) is not read by the child thread backgroundthread. The reason is that the C2 compiler (just in time compiler) of hotspot virtual machine performs loop invariant extrapolation in the process of dynamically compiling bytecode into local machine code (loop invariant code motion) optimization result: since the shared variable ready in the demo is not decorated with volatile, the C2 compiler will think that the variable will not be accessed by multiple threads (in fact, multiple threads access the variable), so the C2 compiler will countingthread. Run() to improve code execution efficiency The while loop statement in is optimized to machine code equivalent to the following pseudo code:

    <span class="token keyword">while<span class="token punctuation">(<span class="token boolean">true<span class="token punctuation">)<span class="token punctuation">{

    count<span class="token operator">++<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token punctuation">}

    This optimization can be confirmed by looking at the assembly code generated by the C2 compiler, as shown in Figure 1. Unfortunately, this optimization leads to an endless loop!

    Figure 1 assembly code generated by loop invariant extrapolation optimization of C2 compiler

    If we use volatile to modify the ready variable in the above demo, the C2 compiler will "realize" that ready is a shared variable, so it will not modify countingthread The while loop statement in run () performs loop invariant extrapolation optimization to avoid dead loops.

    Of course, hardware factors can also cause visibility problems. In order to improve the efficiency of memory write operations, the hardware component store buffer and invalidate queue introduced by the processor may cause the updates made by a thread to shared variables to be unreadable by subsequent threads.

    The Java language specification stipulates that for the same volatile variable, One thread (write thread) updates the variable, and other threads (read thread) then read the variable. These threads can always read the updates made by the write thread to the variable. In other words, if the write thread updates a volatile variable and the read thread reads the variable later, these read threads can read the updates made by the write thread to the variable. This is guaranteed (not a chance!). However, because volatile is not exclusive like a lock, volatile cannot guarantee that the variable value read by the read thread is the latest value of the shared variable: when the read thread reads a volatile variable, other threads (write thread) may have just updated the variable, so the shared variable value read by the read thread is only a relatively new value, that is, the value updated by other threads (not necessarily the latest value).

    4. Summary

    Above, we introduced the atomicity guarantee and visibility guarantee of volatile keyword for long / double variable access operation. Next, we will introduce volatile's guarantee of order, and deeply understand volatile's guarantee of visibility and order by introducing the concept of happens before relationship in JAVA memory model.

    5. Ensure order

    A set of operations performed by threads on one processor may appear out of order to threads on other processors, that is, the perceived order (observed order) of each operation in this set of operations by these threads is inconsistent with the program order (the order specified in the object code).

    Let's look at an out of order experiment, as shown in Listing 4.

    Listing 4. JIT compiler instruction reordering 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">/*

    • 由于我们未对共享变量helper进行任何处理(比如采用volatile关键字修饰该变量),

    • 因此,这里可能存在可见性问题,即当前线程读取到的变量值可能为null。

    */

    <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">}

    The program in Listing 4 is very simple (the reader can ignore the annotation because it is for the test tool): the createhelper method updates the instance variable helper to a newly created helper instance; the consume method reads the helper instance referenced by the helper and calculates all the fields of the instance The sum of the values of (payloada ~ payloadd) is used as its return value. The main method of the program calls the runtest method of the test tool testrunner to make the test tool schedule some threads to execute the createhelper method and the consume method concurrently, and count the return values of the consume method executed multiple times. Because the constructor parameter extern used by the createhelper method when creating a helper instance The aldata value is 1, so it seems "natural" that the return value of the consume method should be 4. However, this is not always the case. Run the program shown in Listing 4 [1] in server mode and set the Java virtual machine parameter "- XX: - usecompressedoops" with the following command:

    java -server -XX:-UseCompressedOops JITReorderingDemo

    We can see the following output [2]:

    expected:-1 occurrences:8 ==>Helper is null
    

    expected:0 occurrences:2 ==>Helper is not null,but it is not initialized

    expected:1 occurrences:0 ==>Only 1 field of Helper instance was initialized

    expected:2 occurrences:1 ==>Only 2 fields of Helper instance were initialized

    expected:3 occurrences:4 ==>Only 3 fields of Helper instance were initialized

    expected:4 occurrences:199985 ==>Helper instance was fully initialized

    In the above output, the number after expected represents the return value of the consume method, and the corresponding occurrences represents the number of times the corresponding return value occurs.

    It's not hard to see that when the program runs this time, Several times, the return value of the consumption method is not 4: some are 3 (4 times), some are 2 (1 time), and even some are 0 (2 times). This indicates that the execution thread of the consumption method sometimes reads an uninitialized result (or initializing) helper instance: the helper instance is not null, but the field values of some of its instance fields are still their default values rather than the initial values specified in the constructor of the helper class. Let's analyze the reasons.

    As we know, the only statement in the createhelper method:

    helper = new Helper(externalData);

    It can be decomposed into the following sub operations (pseudo code representation):

    objRef = allocate(Helper.class);//子操作①:分配Helper实例所需的内存空间,并获得一个指向该空间的引用
    

    inovkeConstructor(objRef);//子操作②:调用Helper类的构造器初始化objRef引用指向的Helper实例

    helper = objRef;//子操作③:将Helper实例引用objRef赋值给实例变量helper

    By looking at the Java bytecode, it is not difficult to find that the program sequence specified in the createhelper method is the above. First initialize the helper instance (sub operation ②) and then assign the reference of the corresponding instance to the instance variable helper (sub operation ③). However, the execution thread of the consume method observed an uninitialized helper instance, which indicates that the thread's perception order of the operations performed by the createhelper method is inconsistent with the program order specified by the method, that is, out of order.

    Looking at the assembly code (equivalent to machine code) dynamically generated by the JIT compiler during the operation of the above program, as shown in Figure 2, we can find that the JIT compiler does not generate the corresponding machine code according to the above source code sequence (here is also the program sequence) every time it compiles bytecode (assembly code): the JIT compiler rearranges the corresponding instruction of sub operation ③ to the corresponding instruction of sub operation ②, that is, the JIT compiler may have written the reference to the helper instance variable before initializing the helper instance. This causes other threads (the execution thread of the consumer method) to see the helper instance variable (not null), the object referenced by the instance variable may not have been initialized or initialized (that is, the code in the corresponding constructor has not been executed). This explains why when we run the above program, the return value of the consume method is sometimes not 4.

    Figure 2. JIT compiler reorders assembly code fragments in demo

    Although disorder is conducive to give full play to the instruction execution efficiency of the processor, as shown in the above experiments, it may also lead to the problem of program correctness. Therefore, in order to ensure the correctness of the program, sometimes we need to ensure that the perceived order of threads for a group of operations is consistent with the program order of this group of operations, that is, to ensure the order of this group of operations. In the above experiment, in order to ensure that the helper instance seen by the execution thread of the consumption method is always initialized, we need to ensure the order of the operations performed by the createhelper method. Therefore, we only need to modify the instance variable helper with the volatile keyword without the help of locks. Here, the volatile keyword is used to prevent the sub operation ② from being reordered by the JIT compiler and processor (instruction reordering and memory reordering) to the sub operation ③, so as to ensure the order.

    According to the Java language specification, For multiple threads accessing (reading and writing) the same volatile variable, the memory read and write operations performed by one thread (write thread) before writing the volatile variable appear to be orderly in other threads (read threads) reading the volatile variable. Let X and y be general (non volatile) shared variables, whose initial values are 0, V is a volatile variable, whose initial values are false, R1 and R2 are local variables, and threads T1 and T2 access V successively, as shown in Figure 3. Then, T1 updates V and the operations performed before updating V are orderly in T2's view: in T2's view, T1's write operations to x, y and V seem to be performed completely in program order. In other words, If T2 reads the value of V as true, the values of X and Y read by the thread must be 1 and 2 respectively. On the contrary, if V is not a volatile variable, the above guarantee does not exist, that is, when T2 reads the value of V as true, the values of X and Y read by T2 may not be 1 and 2.

    Figure 3 example code of order guarantee of volatile keyword

    In the above example, We assume that only one thread updates v (another thread reads V). If more threads update V concurrently, volatile is not exclusive. Therefore, when T2 reads V, other threads other than T1 may have updated the shared variables X and y, which makes the values of X and Y read by T2 may not be 1 and 2 when the value of V read by T2 is true. However, this phenomenon is data competition This is not in contradiction with volatile's ability to ensure order itself.

    6. Happens before relationship

    Understanding the concept of happens before relationship defined in the JAVA memory model will help us further understand the protection of volatile variables for visibility and ordering.

    The JAVA memory model defines some actions, including variable read / write, lock and unlock, thread start (thread. Start() call) and join (thread. Join() call), etc. If there is a happens before relationship between action a and action B, the execution result of action a is visible to action B. On the contrary, if there is no happens before relationship between action a and action B, the execution result of action a may not be visible to B. Below, we use "→" to represent the happens before relationship. For example, "a → B" indicates that there is a happens before relationship between action a and action B.

    The volatile variable rule in the Java Memory Model stipulates that writing to a volatile variable happens before (subsequence) each read operation for the variable. There are two points to note here: first, there is a happens before relationship between write and read operations for the same volatile variable, and there is no happens before relationship between write and read operations for different volatile variables; second, write and read operations for the same volatile variable must have a chronological relationship, that is A thread writes to another thread before reading, so that there can be a happens before relationship between the two actions. Therefore, for Fig. 2, there can be WV → RV, that is, the result of action WV (writing volatile variable V) is visible to RV (reading volatile variable V).

    The program order rule in the Java Memory Model stipulates that every action in the same thread is happens before, and every action in the thread that is next to the action in the program sequence. Therefore, for Figure 3, there can be the following happens before relationship:

    wX→wY (hb1)
    

    wY→wV(hb2)

    rV→rX (hb3)

    rX→rY(hb4)

    The happens before relation is transitive, that is, if a → B, B → C, then there is a → C. Therefore, the following happens before relationship can be obtained from HB1 and HB2:

    wX→wV(hb5)

    Then, according to the volatile variable rule, there can be a happens before relationship:

    wV→rV(hb6)

    Further, according to the transitivity of the happens before relationship, the following happens before relationship can be obtained from HB5 and HB6:

    wX→rV(hb7)

    Similarly, according to the transitivity of the happens before relationship, the following happens before relationship can be obtained from hb7 and HB3:

    wX→rX(hb8)

    Similarly, we can infer the following happens before relationship:

     wY→rY(hb9)

    Thus, the updates made by thread T1 to the common shared variables X and y are visible to thread T2. This visibility is guaranteed under the joint action of volatile variable rules, program sequence rules and the transitivity of happens before relationship. Therefore, we say that the volatile keyword not only ensures the visibility of the updates made by the writer thread to the volatile variable (HB6), but also ensures the visibility of the updates made by the writer thread to other non volatile variables before writing the volatile variable (HB8 and Hb9).

    After understanding the concept of happens before relationship, we can think about this question: does the volatile keyword guarantee visibility and ordering apply to arrays? For example, for an int array varr decorated with volatile, thread a executes "varr [0] = 1;", Next, thread B reads the first element of varr, So is the element value read by thread B necessarily "1" (here we assume that only thread a and thread B access varr)? The answer is "not necessarily": thread a and thread B are only reading threads from the perspective of volatile keyword (read the volatile variable varr), that is, there is no happens before relationship between the two threads, so the update of the first element of varr by thread a is not necessarily visible to thread B. in this example, to ensure the visibility of the update of array elements, we can use the java.util.concurrent.atomic.atomicintegerarray class.

    7. Summary

    The above introduces volatile's guarantee of order, and further introduces volatile's guarantee of visibility and order by introducing the concept of happens before relationship in JAVA memory model. Through the previous introduction, we know that the role of volatile keyword includes ensuring the atomicity, visibility and order of long / double variable access operations. Next, we will introduce the implementation of volatile keyword in Java virtual machine, the overhead of volatile keyword and typical application scenarios of volatile.

    8. Implementation of volatile in Java virtual machine

    This section will cover many terms, as shown in Table 1.

    Table 1 terms in this section

    The atomicity guarantee of long / double variable access operation of Java virtual machine is realized by using atomic instructions (processor instructions with atomicity). This is further introduced through an experiment. The Java code required for the experiment is shown in Listing 6.

    Listing 6 experimental java code for the implementation of volatile semantics by Java virtual machine
    <span class="token keyword">static <span class="token keyword">long normalLong <span class="token operator">= 0L<span class="token punctuation">;

    <span class="token keyword">static <span class="token keyword">volatile <span class="token keyword">long volatileLong <span class="token operator">= 0L<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 keyword">long v1 <span class="token operator">= <span class="token number">0<span class="token punctuation">,v2 <span class="token operator">= <span class="token number">0<span class="token punctuation">;

    <span class="token keyword">for <span class="token punctuation">(<span class="token keyword">int i <span class="token operator">= <span class="token number">0<span class="token punctuation">; i <span class="token operator">< <span class="token number">100100<span class="token punctuation">; i<span class="token operator">++<span class="token punctuation">) <span class="token punctuation">{

    <span class="token function">normalWrite<span class="token punctuation">(i<span class="token punctuation">)<span class="token punctuation">;

    <span class="token function">volatileWrite<span class="token punctuation">(i<span class="token punctuation">)<span class="token punctuation">;

    v1 <span class="token operator">= <span class="token function">normalRead<span class="token punctuation">(<span class="token punctuation">) <span class="token operator">+ i<span class="token punctuation">;

    v2 <span class="token operator">= <span class="token function">volatileRead<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;

    <span class="token punctuation">}

    System<span class="token punctuation">.out<span class="token punctuation">.<span class="token function">println<span class="token punctuation">(v1 <span class="token operator">+ <span class="token string">"," <span class="token operator">+ v2<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">normalWrite<span class="token punctuation">(<span class="token keyword">long value<span class="token punctuation">) <span class="token punctuation">{

    normalLong <span class="token operator">= value<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">volatileWrite<span class="token punctuation">(<span class="token keyword">long value<span class="token punctuation">) <span class="token punctuation">{

    volatileLong <span class="token operator">= value<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token keyword">public <span class="token keyword">static <span class="token keyword">long <span class="token function">volatileRead<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{

    <span class="token keyword">return volatileLong<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token keyword">public <span class="token keyword">static <span class="token keyword">long <span class="token function">normalRead<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{

    <span class="token keyword">return normalLong<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token punctuation">}

    The machine code (expressed in x86 assembly language) used when the 32-bit Java virtual machine (JIT compiler) performs (dynamically compiles) the normal long / double variable write operation in the normalwrite method is shown in Figure 4.

    Figure 4 implementation of write operation on ordinary long / double variables under x86 processor of 32-bit Java virtual machine

    so The 32-bit Java virtual machine writes ordinary long / double variables (here long variables) on x86 processor platforms through two sub operations - write the lower 32 bits first and then write the higher 32 bits. The 32-bit Java virtual machine may still use a command on some processor platforms (for example, the strd instruction is used on the arm processor platform) to realize the writing operation of ordinary long / double variables, but this instruction may not be an atomic instruction. Therefore, from the Java language level, the writing operation of ordinary long / double variables at this time is also not an atomic operation. 32-bit Java virtual machine (JIT compiler) uses an atomic instruction (vmovsd) to write volatile long / double variables in the volatile write method on the x86 processor platform, as shown in Figure 5.

    Figure 5 implementation of write operation on volatile long / double variables under x86 processor of 32-bit Java virtual machine

    Similarly, the atomicity guarantee of Java virtual machine for long / double variable reading operation is also realized by using atomic instructions. For example, the 32-bit Java virtual machine will use the atomic instruction vmovsd to read volatile modified long / double variables on the X86 platform, while the reading operation of ordinary long / double variables is realized by using two mov instructions.

    The Java virtual machine guarantees visibility and order through the use of memory barriers.

    When a processor performs a memory write operation, it often writes the data to its write buffer first rather than directly to the cache. Since the contents of the write buffer on one processor cannot be read by other processors, Therefore, the write thread must ensure that its updates to volatile variables and its updates to other shared variables before updating volatile variables (hereinafter referred to as updates to shared variables) reach the cache of the processor (instead of staying in the write buffer). In this way, it is possible for these updates of the write thread to be read by threads on other processors through the cache consistency protocol. For this purpose, the Java virtual machine (JIT compiler) will insert a storeload memory barrier after the volatile variable write operation. One of the functions of this memory barrier is to write the current contents in the write buffer of its execution processor to the cache.

    Due to the existence of the invalidation queue, the shared variable values read by the processor from its cache may be outdated. So, To ensure that the read thread can read the updates made by the write thread to the shared variables (including volatile variables), the execution processor of the read thread must ensure that the contents in the invalidation queue are applied to the cache of the processor before reading the volatile variables, that is, set the corresponding cache line in the processor to be invalid according to the contents in the invalidation queue, so that the updates made by the write thread to the shared variables can be reflected on the cache of the processor 。 For this reason, the Java virtual machine (JIT compiler) will insert a loadload memory barrier before the volatile variable read operation. Some processors (such as x86 processor and arm processor) do not introduce the invalidation queue, so the above loadload memory barrier is no longer needed on these processors.

    It can be seen that the visibility guarantee of volatile keyword is realized through the paired use of memory barrier in write thread and read thread by Java virtual machine (JIT compiler), as shown in Figure 6.

    Figure 6 memory barrier inserted by Java virtual machine (JIT compiler) to implement volatile semantics

    The volatile keyword guarantees the order through the Java virtual machine (JIT compiler) pairing the memory barrier between the write thread and the read thread. In order to make the updates made by the write thread to the shared variables look orderly to the read thread (that is, the perceived order is consistent with the program order). The Java virtual machine must first ensure that the program order of the writing thread is ahead of the writing of volatile variables. The updates to other shared variables are reflected on the cache of the processor where the thread is located before the updates to volatile variables. In other words, the Java virtual machine must ensure that the program order is ahead of the writing of volatile variables Other write operations of cannot be reordered by the compiler / processor after the volatile variable write operation through instruction reordering and / or memory reordering. For this purpose, the Java virtual machine (JIT compiler) will insert loadstore + storestore memory barrier before volatile variable write operation. This combined memory barrier prohibits reordering (including instruction reordering and memory reordering) between volatile variable write operation and any read and write operation before the operation. Secondly, Java virtual machine (JIT compiler) must ensure that the read thread starts to read the updates made by the write thread to the volatile variable after reading the updates made by the write thread to other shared variables before updating the volatile variable. In other words, the Java virtual machine must ensure that the read and write operations of other shared variables listed after the read operations of the volatile variable in the program sequence cannot be processed by the compiler / processor For this reason, the Java virtual machine (JIT compiler) will insert a loadload + loadstore memory barrier after the volatile variable read operation. This combined memory barrier prohibits the reordering between the volatile variable read operation and any read and write operations after the operation (including instruction reordering and memory reordering). It can be seen that the Java virtual machine guarantees the order of volatile by pairing write threads and read threads to use the memory barrier, as shown in Figure 6.

    Figure 7 and figure 8 show the memory barrier inserted when the Java virtual machine (32-bit) jits the volatilewrite and volatileread methods in Listing 1 on the arm processor platform.

    Figure 7 memory barrier inserted by Java virtual machine before and after volatile variable write operation under arm processor platform

    In Figure 7, the "DMB sy" instruction inserted by the JIT compiler before the volatile write operation ("vstr D7, [R5, #96]" instruction) is equivalent to the loadstore + storestore memory barrier. The "DSB sy" instruction inserted by the JIT compiler after the volatile write operation is equivalent to the storeload memory barrier.

    Figure 8 memory barrier inserted by Java virtual machine after volatile variable read operation under arm processor platform

    In Figure 8, the "DMB sy" instruction inserted by the JIT compiler after the volatile read operation ("vldr D7, #96]" instruction) is equivalent to the loadstore + loadload memory barrier. Since the arm processor does not use the invalidation queue, the JIT compiler does not need to insert the loadload memory barrier before the volatile read operation.

    9. Volatile overhead

    In the last section, we talked about the Java virtual machine (JIT compiler) will insert a storeload memory barrier after the volatile variable write operation. The storeload memory barrier is an all-round memory barrier, which is the most powerful and expensive memory barrier in the memory barrier. In addition to writing the entries in the write buffer to the cache, the memory barrier can also apply the contents of the invalidation queue to the high buffer Cache. These two operations are expensive, On some processors, such as ARM processors, this memory barrier may also cause processor pipelining (pipeline) pause. Since the Java virtual machine does not need to insert a memory barrier after the normal variable write operation, and the write operation in the critical area has the cost of not only the memory barrier, but also the cost of lock application and release. Therefore, the cost of volatile variable write operation is between the normal variable write operation and the write operation in the critical area.

    If the processor introduces an invalidation queue, the Java virtual machine needs to insert a loadload memory barrier before the volatile variable read operation. In addition, The loadload + loadstore memory barrier inserted by the Java virtual machine (JIT compiler) after the volatile variable read operation prevents the processor from performing some optimizations (such as reordering and preloading data). The read operation in the critical area not only has the cost of memory barrier, but also the cost of lock application and release. Therefore, the cost of volatile variable read operation is between ordinary variable read operation and read operation in the critical area.

    The values of common shared variables may be cached into registers by the JIT compiler, that is, for any thread, The first time the thread reads a common shared variable is a memory read operation (for example, MOV instruction on x86 processor), and then repeatedly reading the shared variable is from the register. According to the semantics of volatile keyword, volatile variables cannot be cached in the register, that is, each volatile variable read operation is a memory read operation, and even if the same thread reads the same volatile variable several times in a row, each read in the register All fetch operations are read from memory. Therefore, on the whole, the reading cost of volatile variables is greater than that of ordinary shared variables.

    10. Typical application scenarios of volatile

    10.1. Indirectly guarantee the atomicity and visibility of composite operation

    For the visibility and atomicity problem in Listing 2, although we can solve it by locking the updatehostinfo method and connecttohost method, with the volatile keyword, we can not only ensure the visibility and atomicity, but also avoid the cost of locking, as shown in Listing 7.

    Listing 7 uses volatile to indirectly guarantee the atomicity and visibility of composite operations
    <span class="token keyword">private <span class="token keyword">volatile HostInfo hostInfo<span class="token punctuation">;

    <span class="token keyword">public <span class="token keyword">void <span class="token function">updateHostInfo<span class="token punctuation">(String ip<span class="token punctuation">,<span class="token keyword">int port<span class="token punctuation">) <span class="token punctuation">{

    HostInfo newHostInfo <span class="token operator">= <span class="token keyword">new <span class="token class-name">HostInfo<span class="token punctuation">(ip<span class="token punctuation">,port<span class="token punctuation">)<span class="token punctuation">;

    <span class="token keyword">this<span class="token punctuation">.hostInfo <span class="token operator">= newHostInfo<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token keyword">public <span class="token keyword">void <span class="token function">connectToHost<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{

    String ip <span class="token operator">= hostInfo<span class="token punctuation">.<span class="token function">getIp<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;

    <span class="token keyword">int port <span class="token operator">= hostInfo<span class="token punctuation">.<span class="token function">getPort<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;

    <span class="token function">connectToHost<span class="token punctuation">(ip<span class="token punctuation">,<span class="token keyword">int port<span class="token punctuation">) <span class="token punctuation">{

    <span class="token keyword">this<span class="token punctuation">.ip <span class="token operator">= ip<span class="token punctuation">;

    <span class="token keyword">this<span class="token punctuation">.port <span class="token operator">= port<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token keyword">public String <span class="token function">getIp<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{

    <span class="token keyword">return ip<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token keyword">public <span class="token keyword">int <span class="token function">getPort<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{

    <span class="token keyword">return port<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token comment">// ...

    <span class="token punctuation">}

    <span class="token punctuation">}

    The updatehostinfo method in Listing 7 uses the immutable object mode: when updating the host IP address and port number, it does not call the corresponding set method of the hostinfo class, but creates a new hostinfo instance first, and then updates the instance (reference of) is assigned to the instance variable hostinfo to update the host information. Since this assignment operation itself is an atomic operation, we only need to make the result of this assignment operation visible to other threads to ensure thread safety. Therefore, we only need to declare the instance variable hostinfo as a volatile variable.

    10.2 safety release of guaranteed objects

    A typical application of volatile is to correctly implement the singleton based on the double checked locking method, as shown in Listing 3. The purpose of using the double checked locking method to implement the singleton is to realize delayed loading (lazy load to reduce unnecessary overhead) it can also minimize the cost of locks. In Listing 8, volatile is used to modify the static variable instance for two purposes: to ensure visibility and order. Although the assignment of the instance variable is carried out in a critical area, the if statement checked for the first time is not in the critical area. That is, statement ③ is right There is no happens before relationship between the write operation of instance and the read operation of statement ① on instance. Therefore, the execution result of statement ③ is not necessarily visible to statement ①. In order to ensure that the result of the write operation of statement ③ to instance is visible to statement ① (the first check), we only need to modify instance with volatile. From the perspective of order, even if the execution thread of statement ① reads that instance is not null without modifying instance with volatile (the result of statement ③ executed by other threads), then due to reordering (JIT reordering and / or live memory reordering), the object referenced by instance may still be uninitialized, which may lead to the correctness of the program. After modifying instance with volatile, under the effect of volatile ensuring order, once the execution thread of statement ① sees that instance is not null, the object referenced by instance must be initialized It's over. At this point, we call the object referenced by instance to be published safely.

    Listing 8 uses volatile to correctly implement a singleton class based on double check locking
    <span class="token keyword">private <span class="token keyword">static <span class="token keyword">volatile DCLSingleton instance<span class="token punctuation">;

    <span class="token comment">// 省略其他字段

    <span class="token comment">// 私有构造器

    <span class="token keyword">private <span class="token function">DCLSingleton<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{

    <span class="token punctuation">}

    <span class="token keyword">public DCLSingleton <span class="token function">getInstance<span class="token punctuation">(<span class="token punctuation">) <span class="token punctuation">{

    <span class="token keyword">if <span class="token punctuation">(null <span class="token operator">== instance<span class="token punctuation">) <span class="token punctuation">{<span class="token comment">// 语句①: 第1次检查,不加锁

    <span class="token keyword">synchronized <span class="token punctuation">(DCLSingleton<span class="token punctuation">.<span class="token keyword">class<span class="token punctuation">) <span class="token punctuation">{

    <span class="token keyword">if <span class="token punctuation">(null <span class="token operator">== instance<span class="token punctuation">) <span class="token punctuation">{<span class="token comment">// 语句②: 第2次检查,加锁

    instance <span class="token operator">= <span class="token keyword">new <span class="token class-name">DCLSingleton<span class="token punctuation">(<span class="token punctuation">)<span class="token punctuation">;<span class="token comment">// 语句③:实例化

    <span class="token punctuation">}

    <span class="token punctuation">}

    <span class="token punctuation">}

    <span class="token keyword">return instance<span class="token punctuation">;

    <span class="token punctuation">}

    <span class="token comment">// 省略其他public方法

    <span class="token punctuation">}

    11. Summary

    The volatile keyword is used to ensure the atomicity, visibility and order of long / double variable access operations. When implementing the semantics of volatile keyword, Java virtual machine usually uses some special processor instructions (atomic instructions and memory barriers). The overhead of volatile variable access is between ordinary variable access and variable access in critical areas. Typical application scenarios of volatile include indirectly ensuring the atomicity of composite operations, ensuring the safe release of objects, etc.

    12. References

    1. Huang Wenhai Practical guide to Java multithreaded programming (core). Electronic Industry Press, 2017

    2. Huang Wenhai Practical guide to Java multithreaded programming (design patterns). Electronic Industry Press, 2015

    3. Brian Goetz et al Java Concurrency In Practice. Addison-Wesley Professional,2006

    4. Java language specification Chapter 17:

    5、 Managing volatility:

    6. Practical guide to Java multithreaded programming mode (II): immutable object mode:

    7、 Java Memory Model From a Programmer's Point-of-View:

    8、 The JSR-133 Cookbook for Compiler Writers:

    9、 Memory Barriers: a Hardware View for Software Hackers:

    10、Memory Barriers and JVM Concurrency:

    [1] Virtual machine parameter "- XX: - usecompressedoops": https://docs.oracle.com/javase/8/docs/technotes/guides/vm/performance-enhancements-7.html#compressedOop 。

    [2] This outputs the corresponding execution environment information - operating system: Linux (x86_64 system), JDK version: JDK 1.8.0_40, processor model: Intel i5-3210m.

    The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
    THE END
    分享
    二维码
    < <上一篇
    下一篇>>