JMM 线程内存模型简介 和 volatile 关键字底层原理简析

一、CPU 缓存架构模型图和 JMM 线程内存模型图:

在讲解之前,先区别两个概念:JVM 内存模型和 JMM。

Java 内存模型准确讲应该叫 Java 线程内存模型(Java Memory Model 缩写为 JMM),JMM 的目的是为了解决 Java 多线程对共享数据的读写一致性问题,通过 Happens-Before 语义定义了 Java 程序对数据的访问规则,解决不同 CPU 读写冲突导致的 CPU Cache 数据不一致的问题。这是一种逻辑抽象,并无对应内存实体。

JVM 内存模型准确讲应该叫运行时数据区(Run-Time Data Areas),具体是指 JVM 运行过程中的数据区域,此为实实在在存在着的内存区域。

JMM 只是一种逻辑抽象概念,没有与其对应的内存运行区域,两个根本不是一回事!所以,不要将两者概念混淆,也不要轻易尝试在理解分析 JMM 的同时再去分析 Run-Time Data Areas 的机制,否则,你真的会疯,真的会疯,真的会疯(重要的事情说三遍)。当然,如果你是大神,非要按照 JMM 对一段源码进行分析的同时去分析那段代码在 JVM 内存模型的运行流程,那——请受小弟我五体投地一拜!

“掷地铿锵嗟有力,杳无声处见刀光。全凭借,三言两语,便道尽,JMM 模型”。话说当年,CPU 是直接跟主内存相互通讯的,CPU 计算速度很快,但主内存读写速度很慢,所以数据的处理速度主要由主内存决定,主内存的读写速度限制了数据处理的速度。为了解决这个问题,CPU 厂商很快发明了 CPU 高速缓存技术替代了之前教为鸡肋的数据交互方式。Java 中 JMM 线程内存模型跟 CPU 缓存模型相似,是基于后者建立起来的。Java 线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

JMM 中可以简单理解为一个 CPU 运行一个线程,n 个的 CPU 可同时运行 n 个线程。工作内存可以认为是 CPU 中的寄存器 + 高速缓存。

有了 JMM 的一个大致概念,我们再来看看下面这段代码:

当不用 volatile 修饰 TestVolatileVisibility 类的成员变量 flag 时:

 1 public class TestVolatileVisibility {
 2     
 3     private static boolean flag; //默认为false
 4     
 5     public static void main(String[] args) {
 6         
 7         new Thread(new Runnable() { //开启读线程
 8             public void run() {
 9                 System.out.println("waiting data...");
10                 while (!flag) {
11                     
12                 }
13                 System.out.println("========completed=========");
14             }
15         }).start();
16         
17         try {
18             Thread.sleep(2000); //让主线程睡眠2秒,保证flag = false,使读线程先进入循环,写线程再开始执行
19         } catch (InterruptedException e) {
20             e.printStackTrace();
21         }
22         
23         new Thread(new Runnable() { //开启写线程
24             public void run() {
25                 prepareData(); //调用prepareData()方法改写flag的值
26             }
27         }).start();
28         
29     }
30 
31     public static void prepareData() {
32         System.out.println("prepareing data...");
33         flag = true;
34         System.out.println("data is prepared");
35     }
36 }
37 
38 运行结果:
39 
40 waiting data...
41 prepareing data...
42 data is prepared

看到此运行结果我们可能会产生疑问了:怎么“========completed=========”这行字符串没被打印?

没打印,就说明读线程还未结束,陷入了死循环,线程中的 flag 变量依然为 false。很奇怪,我们不是已经在写线程中对 flag 重新赋值为 true 了么?怎么读线程还陷在死循环中?

其实没什么好奇怪的。 由于 JMM 的控制,Java 中的线程不能直接相互通信,读线程是看不到写线程对 flag 的改变的,写线程改变的只是在 CPU 缓存中的 flag 的一个副本,改变后的 flag 副本的值通过后续的一系列原子操作再重新赋给主内存中的 flag;而与此同时,读线程里的 flag 副本仍为 false ,所以读线程未跳出循环,既然还没跳出循环便不会执行接下来的 System.out.println("========completed=========") 这行语句。文字说明可能让人有些迷糊,接下来我准备上图,在底层上仔细地分析上面那段代码。不过上图前,让我们先看看上面那段没用 volatile 修饰 flag 的 Java 源码的汇编指令。

从网络上寻找并下载 hsdis-amd64.dll 这个东东,然后将 hsdis-amd64.dll 放到 jdk\jre\bin\server 文件夹下,加上如下 JVM 运行参数(hsdis的使用方式可自行百度,网络上教程很多):

-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*TestVolatileVisibility.prepareData

执行 .class 文件即可打印汇编指令(我记下这些汇编指令只是想留待后续研究学习,代码过长,可跳过这些汇编指令,直接看之后的重点):

CompilerOracle: compileonly *TestVolatileVisibility.prepareData
waiting data...
Loaded disassembler from C:\Program Files\Java\jdk1.8.0_221\jre\bin\server\hsdis-amd64.dll
Decoding compiled method 0x0000000003162710:
Code:
[Disassembling for mach='i386:x86-64']
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x00000000179f0540} 'prepareData' '()V' in 'TestVolatileVisibility'
  #           [sp+0x40]  (sp of caller)
  0x00000000031628a0: mov    %eax,-0x6000(%rsp)
  0x00000000031628a7: push   %rbp
  0x00000000031628a8: sub    $0x30,%rsp
  0x00000000031628ac: movabs $0x179f13b0,%r8    ;   {metadata(method data for {method} {0x00000000179f0540} 'prepareData' '()V' in 'TestVolatileVisibility')}
  0x00000000031628b6: mov    0xdc(%r8),%edx
  0x00000000031628bd: add    $0x8,%edx
  0x00000000031628c0: mov    %edx,0xdc(%r8)
  0x00000000031628c7: movabs $0x179f0538,%r8    ;   {metadata({method} {0x00000000179f0540} 'prepareData' '()V' in 'TestVolatileVisibility')}
  0x00000000031628d1: and    $0x0,%edx
  0x00000000031628d4: cmp    $0x0,%edx
  0x00000000031628d7: je     0x0000000003162988
  0x00000000031628dd: movabs $0xd5f00c78,%rsi   ;   {oop(a 'java/lang/Class' = 'java/lang/System')}
  0x00000000031628e7: mov    0x6c(%rsi),%edx    ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@0 (line 32)

  0x00000000031628ea: cmp    (%rdx),%rax        ; implicit exception: dispatches to 0x000000000316299f
  0x00000000031628ed: mov    %rdx,%r8
  0x00000000031628f0: movabs $0x179f13b0,%rdi   ;   {metadata(method data for {method} {0x00000000179f0540} 'prepareData' '()V' in 'TestVolatileVisibility')}
  0x00000000031628fa: movabs $0x1000296e8,%r10  ;   {metadata('java/io/PrintStream')}
  0x0000000003162904: mov    %r10,0x110(%rdi)
  0x000000000316290b: addq   $0x1,0x118(%rdi)
  0x0000000003162913: movabs $0xd6199ba8,%r8    ;   {oop("prepareing data...")}
  0x000000000316291d: mov    %rsi,0x20(%rsp)
  0x0000000003162922: nop
  0x0000000003162923: nop
  0x0000000003162924: nop
  0x0000000003162925: nop
  0x0000000003162926: nop
  0x0000000003162927: callq  0x00000000030a61a0  ; OopMap{[32]=Oop off=140}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)
                                                ;   {optimized virtual_call}
  0x000000000316292c: movabs $0xd5fb1440,%r8    ;   {oop(a 'java/lang/Class' = 'TestVolatileVisibility')}
  0x0000000003162936: movb   $0x1,0x68(%r8)     ;*putstatic flag
                                                ; - TestVolatileVisibility::prepareData@9 (line 33)

  0x000000000316293b: mov    0x20(%rsp),%rsi
  0x0000000003162940: mov    0x6c(%rsi),%edx    ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@12 (line 34)

  0x0000000003162943: cmp    (%rdx),%rax        ; implicit exception: dispatches to 0x00000000031629a4
  0x0000000003162946: mov    %rdx,%r8
  0x0000000003162949: movabs $0x179f13b0,%rsi   ;   {metadata(method data for {method} {0x00000000179f0540} 'prepareData' '()V' in 'TestVolatileVisibility')}
  0x0000000003162953: movabs $0x1000296e8,%r10  ;   {metadata('java/io/PrintStream')}
  0x000000000316295d: mov    %r10,0x140(%rsi)
  0x0000000003162964: addq   $0x1,0x148(%rsi)
  0x000000000316296c: movabs $0xd6199bf8,%r8    ;   {oop("data is prepared")}
  0x0000000003162976: nop
  0x0000000003162977: callq  0x00000000030a61a0  ; OopMap{off=220}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)
                                                ;   {optimized virtual_call}
  0x000000000316297c: add    $0x30,%rsp
  0x0000000003162980: pop    %rbp
  0x0000000003162981: test   %eax,-0x2392887(%rip)        # 0x0000000000dd0100
                                                ;   {poll_return}
  0x0000000003162987: retq   
  0x0000000003162988: mov    %r8,0x8(%rsp)
  0x000000000316298d: movq   $0xffffffffffffffff,(%rsp)
  0x0000000003162995: callq  0x00000000031606a0  ; OopMap{off=250}
                                                ;*synchronization entry
                                                ; - TestVolatileVisibility::prepareData@-1 (line 32)
                                                ;   {runtime_call}
  0x000000000316299a: jmpq   0x00000000031628dd
  0x000000000316299f: callq  0x000000000315af00  ; OopMap{rsi=Oop rdx=Oop off=260}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)
                                                ;   {runtime_call}
  0x00000000031629a4: callq  0x000000000315af00  ; OopMap{rdx=Oop off=265}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)
                                                ;   {runtime_call}
  0x00000000031629a9: nop
  0x00000000031629aa: nop
  0x00000000031629ab: mov    0x2a8(%r15),%rax
  0x00000000031629b2: movabs $0x0,%r10
  0x00000000031629bc: mov    %r10,0x2a8(%r15)
  0x00000000031629c3: movabs $0x0,%r10
  0x00000000031629cd: mov    %r10,0x2b0(%r15)
  0x00000000031629d4: add    $0x30,%rsp
  0x00000000031629d8: pop    %rbp
  0x00000000031629d9: jmpq   0x000000000315a660  ;   {runtime_call}
  0x00000000031629de: hlt    
  0x00000000031629df: hlt    
[Stub Code]
  0x00000000031629e0: nop                       ;   {no_reloc}
  0x00000000031629e1: nop
  0x00000000031629e2: nop
  0x00000000031629e3: nop
  0x00000000031629e4: nop
  0x00000000031629e5: movabs $0x0,%rbx          ;   {static_stub}
  0x00000000031629ef: jmpq   0x00000000031629ef  ;   {runtime_call}
  0x00000000031629f4: nop
  0x00000000031629f5: movabs $0x0,%rbx          ;   {static_stub}
  0x00000000031629ff: jmpq   0x00000000031629ff  ;   {runtime_call}
[Exception Handler]
  0x0000000003162a04: callq  0x000000000315cea0  ;   {runtime_call}
  0x0000000003162a09: mov    %rsp,-0x28(%rsp)
  0x0000000003162a0e: sub    $0x80,%rsp
  0x0000000003162a15: mov    %rax,0x78(%rsp)
  0x0000000003162a1a: mov    %rcx,0x70(%rsp)
  0x0000000003162a1f: mov    %rdx,0x68(%rsp)
  0x0000000003162a24: mov    %rbx,0x60(%rsp)
  0x0000000003162a29: mov    %rbp,0x50(%rsp)
  0x0000000003162a2e: mov    %rsi,0x48(%rsp)
  0x0000000003162a33: mov    %rdi,0x40(%rsp)
  0x0000000003162a38: mov    %r8,0x38(%rsp)
  0x0000000003162a3d: mov    %r9,0x30(%rsp)
  0x0000000003162a42: mov    %r10,0x28(%rsp)
  0x0000000003162a47: mov    %r11,0x20(%rsp)
  0x0000000003162a4c: mov    %r12,0x18(%rsp)
  0x0000000003162a51: mov    %r13,0x10(%rsp)
  0x0000000003162a56: mov    %r14,0x8(%rsp)
  0x0000000003162a5b: mov    %r15,(%rsp)
  0x0000000003162a5f: movabs $0x556f0790,%rcx   ;   {external_word}
  0x0000000003162a69: movabs $0x3162a09,%rdx    ;   {internal_word}
  0x0000000003162a73: mov    %rsp,%r8
  0x0000000003162a76: and    $0xfffffffffffffff0,%rsp
  0x0000000003162a7a: callq  0x00000000553a6d10  ;   {runtime_call}
  0x0000000003162a7f: hlt    
[Deopt Handler Code]
  0x0000000003162a80: movabs $0x3162a80,%r10    ;   {section_word}
  0x0000000003162a8a: push   %r10
  0x0000000003162a8c: jmpq   0x00000000030a7600  ;   {runtime_call}
  0x0000000003162a91: hlt    
  0x0000000003162a92: hlt    
  0x0000000003162a93: hlt    
  0x0000000003162a94: hlt    
  0x0000000003162a95: hlt    
  0x0000000003162a96: hlt    
  0x0000000003162a97: hlt    
Decoding compiled method 0x0000000003162310:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x00000000179f0540} 'prepareData' '()V' in 'TestVolatileVisibility'
  #           [sp+0x20]  (sp of caller)
  0x0000000003162460: mov    %eax,-0x6000(%rsp)
  0x0000000003162467: push   %rbp
  0x0000000003162468: sub    $0x10,%rsp         ;*synchronization entry
                                                ; - TestVolatileVisibility::prepareData@-1 (line 32)

  0x000000000316246c: movabs $0xd5f00c78,%rbp   ;   {oop(a 'java/lang/Class' = 'java/lang/System')}
  0x0000000003162476: mov    0x6c(%rbp),%r11d   ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@0 (line 32)

  0x000000000316247a: test   %r11d,%r11d
  0x000000000316247d: je     0x00000000031624cc  ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)

  0x000000000316247f: mov    %r11,%rdx          ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@0 (line 32)

  0x0000000003162482: movabs $0xd6199ba8,%r8    ;   {oop("prepareing data...")}
  0x000000000316248c: data16 xchg %ax,%ax
  0x000000000316248f: callq  0x00000000030a61a0  ; OopMap{rbp=Oop off=52}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)
                                                ;   {optimized virtual_call}
  0x0000000003162494: mov    0x6c(%rbp),%r10d   ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@12 (line 34)

  0x0000000003162498: movabs $0xd5fb1440,%r11   ;   {oop(a 'java/lang/Class' = 'TestVolatileVisibility')}
  0x00000000031624a2: movb   $0x1,0x68(%r11)    ;*putstatic flag
                                                ; - TestVolatileVisibility::prepareData@9 (line 33)

  0x00000000031624a7: test   %r10d,%r10d
  0x00000000031624aa: je     0x00000000031624d9  ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)

  0x00000000031624ac: mov    %r10,%rdx          ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@12 (line 34)

  0x00000000031624af: movabs $0xd6199bf8,%r8    ;   {oop("data is prepared")}
  0x00000000031624b9: xchg   %ax,%ax
  0x00000000031624bb: callq  0x00000000030a61a0  ; OopMap{off=96}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)
                                                ;   {optimized virtual_call}
  0x00000000031624c0: add    $0x10,%rsp
  0x00000000031624c4: pop    %rbp
  0x00000000031624c5: test   %eax,-0x23924cb(%rip)        # 0x0000000000dd0000
                                                ;   {poll_return}
  0x00000000031624cb: retq   
  0x00000000031624cc: mov    $0xfffffff6,%edx
  0x00000000031624d1: xchg   %ax,%ax
  0x00000000031624d3: callq  0x00000000030a57a0  ; OopMap{off=120}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)
                                                ;   {runtime_call}
  0x00000000031624d8: int3   
  0x00000000031624d9: mov    $0xfffffff6,%edx
  0x00000000031624de: nop
  0x00000000031624df: callq  0x00000000030a57a0  ; OopMap{off=132}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)
                                                ;   {runtime_call}
  0x00000000031624e4: int3                      ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)

  0x00000000031624e5: mov    %rax,%rdx
  0x00000000031624e8: jmp    0x00000000031624ed
  0x00000000031624ea: mov    %rax,%rdx          ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)

  0x00000000031624ed: add    $0x10,%rsp
  0x00000000031624f1: pop    %rbp
  0x00000000031624f2: jmpq   0x00000000031613e0  ;   {runtime_call}
  0x00000000031624f7: hlt    
  0x00000000031624f8: hlt    
  0x00000000031624f9: hlt    
  0x00000000031624fa: hlt    
  0x00000000031624fb: hlt    
  0x00000000031624fc: hlt    
  0x00000000031624fd: hlt    
  0x00000000031624fe: hlt    
  0x00000000031624ff: hlt    
[Stub Code]
  0x0000000003162500: movabs $0x0,%rbx          ;   {no_reloc}
  0x000000000316250a: jmpq   0x000000000316250a  ;   {runtime_call}
  0x000000000316250f: movabs $0x0,%rbx          ;   {static_stub}
  0x0000000003162519: jmpq   0x0000000003162519  ;   {runtime_call}
[Exception Handler]
  0x000000000316251e: jmpq   0x000000000315a960  ;   {runtime_call}
[Deopt Handler Code]
  0x0000000003162523: callq  0x0000000003162528
  0x0000000003162528: subq   $0x5,(%rsp)
  0x000000000316252d: jmpq   0x00000000030a7600  ;   {runtime_call}
  0x0000000003162532: hlt    
  0x0000000003162533: hlt    
  0x0000000003162534: hlt    
  0x0000000003162535: hlt    
  0x0000000003162536: hlt    
  0x0000000003162537: hlt    
prepareing data...
data is prepared
========completed=========
Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
View Code

我们可以先认识到这一点:从上面的汇编指令来看,并无 lock 这个汇编前缀指令。好了,上面一大段代码到这里就只得出“无lock前缀指令”这个结论,不理解不要紧,我们再继续来分析程序在底层上运行的流程。

二、JMM 底层的那点事儿

下图描述了之前那段 Java 源码的在底层上执行的流程:

上图中,线程0是读线程,线程1是写线程,可以看到,JMM 中数据的流转是通过一系列原子操作去完成的,对这些原子操作的理解,可凭借上图去学习,这个图我就不再细说了,因为我觉得我画得还算明白吧^_^。这里需要强调的是原子操作的定义——不能被时间片再分割的语句就是原子语句,当执行原子语句时便是原子操作,这种最小执行单元的特性被称为原子性。同一个 CPU 里,在一个或多个连续的时间片中,原子语句要么执行,要么不执行,它一旦执行便不会进入阻塞和就绪状态,直到语句执行结束才能让另外的原子语句执行。所以原子语句在同一个 CPU 中永远是串行的^_^!

那么使用 volatile 修饰成员变量 flag 后又发生了什么“不可描述”的变化?

运行下面代码:

 1 public class TestVolatileVisibility {
 2     
 3     private static volatile boolean flag; //加上volatile关键字
 4     
 5     public static void main(String[] args) {
 6         
 7         new Thread(new Runnable() {
 8             public void run() {
 9                 System.out.println("waiting data...");
10                 while (!flag) {
11                     
12                 }
13                 System.out.println("========completed=========");
14             }
15         }).start();
16         
17         try {
18             Thread.sleep(2000);
19         } catch (InterruptedException e) {
20             e.printStackTrace();
21         }
22         
23         new Thread(new Runnable() {
24             public void run() {
25                 prepareData();
26             }
27         }).start();
28         
29     }
30 
31     public static void prepareData() {
32         System.out.println("prepareing data...");
33         flag = true;
34         System.out.println("data is prepared");
35     }
36 }
37 
38 运行结果:
39 
40 waiting data...
41 prepareing data...
42 data is prepared
43 ========completed========= //这行话被打印出来说明读线程已跳出循环,读线程运行结束

我们可以看到加上 volatile 后,"========completed========="被打印出来了,那么底层到底发生了什么?

要搞清楚产生这种情况的原因,还是得用一张上面程序的内存底层运行流程图来分析,不过在分析前,我任然先使用同样方法打印出了 Java 源码的汇编指令(还是略过这段长代码,看后续的重点):

CompilerOracle: compileonly *TestVolatileVisibility.prepareData
waiting data...
Loaded disassembler from C:\Program Files\Java\jdk1.8.0_221\jre\bin\server\hsdis-amd64.dll
Decoding compiled method 0x0000000002a82910:
Code:
[Disassembling for mach='i386:x86-64']
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x0000000017310540} 'prepareData' '()V' in 'TestVolatileVisibility'
  #           [sp+0x40]  (sp of caller)
  0x0000000002a82aa0: mov    %eax,-0x6000(%rsp)
  0x0000000002a82aa7: push   %rbp
  0x0000000002a82aa8: sub    $0x30,%rsp
  0x0000000002a82aac: movabs $0x173113b0,%r8    ;   {metadata(method data for {method} {0x0000000017310540} 'prepareData' '()V' in 'TestVolatileVisibility')}
  0x0000000002a82ab6: mov    0xdc(%r8),%edx
  0x0000000002a82abd: add    $0x8,%edx
  0x0000000002a82ac0: mov    %edx,0xdc(%r8)
  0x0000000002a82ac7: movabs $0x17310538,%r8    ;   {metadata({method} {0x0000000017310540} 'prepareData' '()V' in 'TestVolatileVisibility')}
  0x0000000002a82ad1: and    $0x0,%edx
  0x0000000002a82ad4: cmp    $0x0,%edx
  0x0000000002a82ad7: je     0x0000000002a82b90
  0x0000000002a82add: movabs $0xd5f00c78,%rsi   ;   {oop(a 'java/lang/Class' = 'java/lang/System')}
  0x0000000002a82ae7: mov    0x6c(%rsi),%edx    ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@0 (line 32)

  0x0000000002a82aea: cmp    (%rdx),%rax        ; implicit exception: dispatches to 0x0000000002a82ba7
  0x0000000002a82aed: mov    %rdx,%r8
  0x0000000002a82af0: movabs $0x173113b0,%rdi   ;   {metadata(method data for {method} {0x0000000017310540} 'prepareData' '()V' in 'TestVolatileVisibility')}
  0x0000000002a82afa: movabs $0x1000296e8,%r10  ;   {metadata('java/io/PrintStream')}
  0x0000000002a82b04: mov    %r10,0x110(%rdi)
  0x0000000002a82b0b: addq   $0x1,0x118(%rdi)
  0x0000000002a82b13: movabs $0xd6199ba8,%r8    ;   {oop("prepareing data...")}
  0x0000000002a82b1d: mov    %rsi,0x20(%rsp)
  0x0000000002a82b22: nop
  0x0000000002a82b23: nop
  0x0000000002a82b24: nop
  0x0000000002a82b25: nop
  0x0000000002a82b26: nop
  0x0000000002a82b27: callq  0x00000000029c61a0  ; OopMap{[32]=Oop off=140}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)
                                                ;   {optimized virtual_call}
  0x0000000002a82b2c: movabs $0xd5fb1440,%r8    ;   {oop(a 'java/lang/Class' = 'TestVolatileVisibility')}
  0x0000000002a82b36: mov    $0x1,%edx
  0x0000000002a82b3b: mov    %dl,0x68(%r8)
  0x0000000002a82b3f: lock addl $0x0,(%rsp)     ;*putstatic flag
                                                ; - TestVolatileVisibility::prepareData@9 (line 33)

  0x0000000002a82b44: mov    0x20(%rsp),%rsi
  0x0000000002a82b49: mov    0x6c(%rsi),%edx    ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@12 (line 34)

  0x0000000002a82b4c: cmp    (%rdx),%rax        ; implicit exception: dispatches to 0x0000000002a82bac
  0x0000000002a82b4f: mov    %rdx,%r8
  0x0000000002a82b52: movabs $0x173113b0,%rsi   ;   {metadata(method data for {method} {0x0000000017310540} 'prepareData' '()V' in 'TestVolatileVisibility')}
  0x0000000002a82b5c: movabs $0x1000296e8,%r10  ;   {metadata('java/io/PrintStream')}
  0x0000000002a82b66: mov    %r10,0x140(%rsi)
  0x0000000002a82b6d: addq   $0x1,0x148(%rsi)
  0x0000000002a82b75: movabs $0xd6199bf8,%r8    ;   {oop("data is prepared")}
  0x0000000002a82b7f: callq  0x00000000029c61a0  ; OopMap{off=228}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)
                                                ;   {optimized virtual_call}
  0x0000000002a82b84: add    $0x30,%rsp
  0x0000000002a82b88: pop    %rbp
  0x0000000002a82b89: test   %eax,-0x1fb2a8f(%rip)        # 0x0000000000ad0100
                                                ;   {poll_return}
  0x0000000002a82b8f: retq   
  0x0000000002a82b90: mov    %r8,0x8(%rsp)
  0x0000000002a82b95: movq   $0xffffffffffffffff,(%rsp)
  0x0000000002a82b9d: callq  0x0000000002a7f8e0  ; OopMap{off=258}
                                                ;*synchronization entry
                                                ; - TestVolatileVisibility::prepareData@-1 (line 32)
                                                ;   {runtime_call}
  0x0000000002a82ba2: jmpq   0x0000000002a82add
  0x0000000002a82ba7: callq  0x0000000002a7af00  ; OopMap{rsi=Oop rdx=Oop off=268}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)
                                                ;   {runtime_call}
  0x0000000002a82bac: callq  0x0000000002a7af00  ; OopMap{rdx=Oop off=273}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)
                                                ;   {runtime_call}
  0x0000000002a82bb1: nop
  0x0000000002a82bb2: nop
  0x0000000002a82bb3: mov    0x2a8(%r15),%rax
  0x0000000002a82bba: movabs $0x0,%r10
  0x0000000002a82bc4: mov    %r10,0x2a8(%r15)
  0x0000000002a82bcb: movabs $0x0,%r10
  0x0000000002a82bd5: mov    %r10,0x2b0(%r15)
  0x0000000002a82bdc: add    $0x30,%rsp
  0x0000000002a82be0: pop    %rbp
  0x0000000002a82be1: jmpq   0x00000000029ed920  ;   {runtime_call}
  0x0000000002a82be6: hlt    
  0x0000000002a82be7: hlt    
  0x0000000002a82be8: hlt    
  0x0000000002a82be9: hlt    
  0x0000000002a82bea: hlt    
  0x0000000002a82beb: hlt    
  0x0000000002a82bec: hlt    
  0x0000000002a82bed: hlt    
  0x0000000002a82bee: hlt    
  0x0000000002a82bef: hlt    
  0x0000000002a82bf0: hlt    
  0x0000000002a82bf1: hlt    
  0x0000000002a82bf2: hlt    
  0x0000000002a82bf3: hlt    
  0x0000000002a82bf4: hlt    
  0x0000000002a82bf5: hlt    
  0x0000000002a82bf6: hlt    
  0x0000000002a82bf7: hlt    
  0x0000000002a82bf8: hlt    
  0x0000000002a82bf9: hlt    
  0x0000000002a82bfa: hlt    
  0x0000000002a82bfb: hlt    
  0x0000000002a82bfc: hlt    
  0x0000000002a82bfd: hlt    
  0x0000000002a82bfe: hlt    
  0x0000000002a82bff: hlt    
[Stub Code]
  0x0000000002a82c00: nop                       ;   {no_reloc}
  0x0000000002a82c01: nop
  0x0000000002a82c02: nop
  0x0000000002a82c03: nop
  0x0000000002a82c04: nop
  0x0000000002a82c05: movabs $0x0,%rbx          ;   {static_stub}
  0x0000000002a82c0f: jmpq   0x0000000002a82c0f  ;   {runtime_call}
  0x0000000002a82c14: nop
  0x0000000002a82c15: movabs $0x0,%rbx          ;   {static_stub}
  0x0000000002a82c1f: jmpq   0x0000000002a82c1f  ;   {runtime_call}
[Exception Handler]
  0x0000000002a82c24: callq  0x0000000002a7cea0  ;   {runtime_call}
  0x0000000002a82c29: mov    %rsp,-0x28(%rsp)
  0x0000000002a82c2e: sub    $0x80,%rsp
  0x0000000002a82c35: mov    %rax,0x78(%rsp)
  0x0000000002a82c3a: mov    %rcx,0x70(%rsp)
  0x0000000002a82c3f: mov    %rdx,0x68(%rsp)
  0x0000000002a82c44: mov    %rbx,0x60(%rsp)
  0x0000000002a82c49: mov    %rbp,0x50(%rsp)
  0x0000000002a82c4e: mov    %rsi,0x48(%rsp)
  0x0000000002a82c53: mov    %rdi,0x40(%rsp)
  0x0000000002a82c58: mov    %r8,0x38(%rsp)
  0x0000000002a82c5d: mov    %r9,0x30(%rsp)
  0x0000000002a82c62: mov    %r10,0x28(%rsp)
  0x0000000002a82c67: mov    %r11,0x20(%rsp)
  0x0000000002a82c6c: mov    %r12,0x18(%rsp)
  0x0000000002a82c71: mov    %r13,0x10(%rsp)
  0x0000000002a82c76: mov    %r14,0x8(%rsp)
  0x0000000002a82c7b: mov    %r15,(%rsp)
  0x0000000002a82c7f: movabs $0x556f0790,%rcx   ;   {external_word}
  0x0000000002a82c89: movabs $0x2a82c29,%rdx    ;   {internal_word}
  0x0000000002a82c93: mov    %rsp,%r8
  0x0000000002a82c96: and    $0xfffffffffffffff0,%rsp
  0x0000000002a82c9a: callq  0x00000000553a6d10  ;   {runtime_call}
  0x0000000002a82c9f: hlt    
[Deopt Handler Code]
  0x0000000002a82ca0: movabs $0x2a82ca0,%r10    ;   {section_word}
  0x0000000002a82caa: push   %r10
  0x0000000002a82cac: jmpq   0x00000000029c7600  ;   {runtime_call}
  0x0000000002a82cb1: hlt    
  0x0000000002a82cb2: hlt    
  0x0000000002a82cb3: hlt    
  0x0000000002a82cb4: hlt    
  0x0000000002a82cb5: hlt    
  0x0000000002a82cb6: hlt    
  0x0000000002a82cb7: hlt    
Decoding compiled method 0x0000000002a82510:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x0000000017310540} 'prepareData' '()V' in 'TestVolatileVisibility'
  #           [sp+0x20]  (sp of caller)
  0x0000000002a82660: mov    %eax,-0x6000(%rsp)
  0x0000000002a82667: push   %rbp
  0x0000000002a82668: sub    $0x10,%rsp         ;*synchronization entry
                                                ; - TestVolatileVisibility::prepareData@-1 (line 32)

  0x0000000002a8266c: movabs $0xd5f00c78,%rbp   ;   {oop(a 'java/lang/Class' = 'java/lang/System')}
  0x0000000002a82676: mov    0x6c(%rbp),%r11d   ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@0 (line 32)

  0x0000000002a8267a: test   %r11d,%r11d
  0x0000000002a8267d: je     0x0000000002a826d0  ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)

  0x0000000002a8267f: mov    %r11,%rdx          ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@0 (line 32)

  0x0000000002a82682: movabs $0xd6199ba8,%r8    ;   {oop("prepareing data...")}
  0x0000000002a8268c: data16 xchg %ax,%ax
  0x0000000002a8268f: callq  0x00000000029c61a0  ; OopMap{rbp=Oop off=52}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)
                                                ;   {optimized virtual_call}
  0x0000000002a82694: movabs $0xd5fb1440,%r10   ;   {oop(a 'java/lang/Class' = 'TestVolatileVisibility')}
  0x0000000002a8269e: movb   $0x1,0x68(%r10)
  0x0000000002a826a3: lock addl $0x0,(%rsp)     ;*putstatic flag
                                                ; - TestVolatileVisibility::prepareData@9 (line 33)

  0x0000000002a826a8: mov    0x6c(%rbp),%r11d   ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@12 (line 34)

  0x0000000002a826ac: test   %r11d,%r11d
  0x0000000002a826af: je     0x0000000002a826dd  ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)

  0x0000000002a826b1: mov    %r11,%rdx          ;*getstatic out
                                                ; - TestVolatileVisibility::prepareData@12 (line 34)

  0x0000000002a826b4: movabs $0xd6199bf8,%r8    ;   {oop("data is prepared")}
  0x0000000002a826be: nop
  0x0000000002a826bf: callq  0x00000000029c61a0  ; OopMap{off=100}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)
                                                ;   {optimized virtual_call}
  0x0000000002a826c4: add    $0x10,%rsp
  0x0000000002a826c8: pop    %rbp
  0x0000000002a826c9: test   %eax,-0x1fb26cf(%rip)        # 0x0000000000ad0000
                                                ;   {poll_return}
  0x0000000002a826cf: retq   
  0x0000000002a826d0: mov    $0xfffffff6,%edx
  0x0000000002a826d5: xchg   %ax,%ax
  0x0000000002a826d7: callq  0x00000000029c57a0  ; OopMap{off=124}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)
                                                ;   {runtime_call}
  0x0000000002a826dc: int3                      ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)

  0x0000000002a826dd: mov    $0xfffffff6,%edx
  0x0000000002a826e2: nop
  0x0000000002a826e3: callq  0x00000000029c57a0  ; OopMap{off=136}
                                                ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)
                                                ;   {runtime_call}
  0x0000000002a826e8: int3                      ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@5 (line 32)

  0x0000000002a826e9: mov    %rax,%rdx
  0x0000000002a826ec: jmp    0x0000000002a826f1
  0x0000000002a826ee: mov    %rax,%rdx          ;*invokevirtual println
                                                ; - TestVolatileVisibility::prepareData@17 (line 34)

  0x0000000002a826f1: add    $0x10,%rsp
  0x0000000002a826f5: pop    %rbp
  0x0000000002a826f6: jmpq   0x0000000002a815e0  ;   {runtime_call}
  0x0000000002a826fb: hlt    
  0x0000000002a826fc: hlt    
  0x0000000002a826fd: hlt    
  0x0000000002a826fe: hlt    
  0x0000000002a826ff: hlt    
[Stub Code]
  0x0000000002a82700: movabs $0x0,%rbx          ;   {no_reloc}
  0x0000000002a8270a: jmpq   0x0000000002a8270a  ;   {runtime_call}
  0x0000000002a8270f: movabs $0x0,%rbx          ;   {static_stub}
  0x0000000002a82719: jmpq   0x0000000002a82719  ;   {runtime_call}
[Exception Handler]
  0x0000000002a8271e: jmpq   0x0000000002a7a960  ;   {runtime_call}
[Deopt Handler Code]
  0x0000000002a82723: callq  0x0000000002a82728
  0x0000000002a82728: subq   $0x5,(%rsp)
  0x0000000002a8272d: jmpq   0x00000000029c7600  ;   {runtime_call}
  0x0000000002a82732: hlt    
  0x0000000002a82733: hlt    
  0x0000000002a82734: hlt    
  0x0000000002a82735: hlt    
  0x0000000002a82736: hlt    
  0x0000000002a82737: hlt    
prepareing data...
data is prepared
========completed=========
Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
View Code

仔细观察你会发现上面的汇编代码中终于出现 lock 这个前缀指令了:

 0x0000000002a82b3f: lock addl $0x0,(%rsp)     ;*putstatic flag
                                                ; - TestVolatileVisibility::prepareData@9 (line 33)

0x0000000002bb163f: lock addl $0x0,(%rsp) —— 汇编代码(rsp 代表 x64 CPU 中的寄存器)

*putstatic flag —— 对应的JVM指令码,意思是:为指定的类(TestVolatileVisibility)的静态域变量 flag 赋值

- TestVolatileVisibility::prepareData@9 (line 33) ——对应的 Java 源码的位置

对应的 Java 源码(line 33):

flag = true;

可以看到与 flag = true 这条赋值语句对应的汇编指令就是 lock addl $0x0,(%rsp)。若再对应 JMM 模型中数据的原子操作就是 store --> write。

IA-32 架构软件开发者手册对 lock 前缀指令的解释(博主并未查询手册,因为我不懂汇编,感兴趣的同学可自行查询):

lock 指令在多核处理器下会触发以下这些重要的事件:

1、由执行写线程(即修改共享内存)的 CPU 锁定主内存中的共享变量

CPU 收到 lock 前缀指令后,底层将通过两种加锁方式锁定共享内存:
(1)在 CPU 将修改后的数据副本回写到主内存的这段流程期间对共享内存加锁(缓存行加锁),对应 JMM 数据原子操作为:lock --> store --> write --> unlock;
(2)在 CPU 读写共享内存的整个流程期间对共享内存加锁(总线加锁),对应 JMM 数据原子操作为:lock --> read --> load --> use --> assign --> store -->  write --> unlock;

2、将当前处理器缓存行的数据立即写回到系统主内存中(不论写线程里的修改操作之后有多少个语句,CPU 会立即将共享变量副本回写到主内存里,回写完成再执行后续语句)。

3、这个写回内存的操作会引起在其他 CPU 里缓存了该内存地址的数据失效。

使用 volatile 修饰共享变量 flag 后,就不得不谈谈总线嗅探机制和 MESI 缓存一致性协议了,在这里我没有能力过多且深入地探讨这两个知识点,网络上关于总线嗅探机制和 MESI 协议的文章很多,感兴趣的可自行搜索学习。下面将会按照之前我写的 Java 源码来分析。

三、JMM 中存在 CPU 缓存不一致的问题

这里我想要表达的是,JVM 会按照 JMM 线程内存模型来完成线程之间通信,即线程之间(不同 CPU 缓存之间)以共享内存的机制来交换信息。当我们不去主动解决(如加上 volatile 关键字)多个 CPU 缓存不一致问题时, JMM 中便会自然而然地出现缓存不一致的问题。

四、MESI 缓存一致性协议和总线嗅探机制

底层为保证多核处理器不同核心缓存的一致性提供了两种解决方案:

1、总线加锁

CPU 从主内存读取数据到高速缓存中,会在总线对这个数据加锁,这样其他 CPU 就没办法读或写这个数据,直到这个 CPU 使用完数据释放锁之后,其他 CPU 才能读写该数据。

加上 volatile 关键字后,早期的处理器会采用总线加锁这种方式去保证 CPU 缓存的一致性,但这种处理方式几乎等同于多个线程串行运行,运行效率过低,不能更好地发挥多核处理器的并行处理能力,所以随着 CPU 硬件技术的发展将逐渐被淘汰。

2、MESI 缓存一致性协议

MESI 是属于底层上的协议,其确切叫法应该是 MESI 多核处理器缓存一致性协议。

当我们加上 volatile 后,整个处理器结构便严格执行 MESI 协议,多个 CPU 从主内存已读取了同一个数据到各自的高速缓存中(这时各核心缓存里的这个数据是一致的),若其中某个写线程所在的 CPU 修改了缓存里的数据(共享变量副本),会立即启用缓存行加锁机制,而接下来该数据会马上同步回主内存中,其他 CPU 通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。

3、MESI 缓存一致性协议的失效

(1)当缓存行存储的数据超过最小存储单元大小时(数据长度存储跨越多个缓存行的情况),就会导致 MESI 缓存一致性协议失效;
(2)若底层系统不支持缓存一致性协议,那么 MESI 协议也会失效。

缓存行是 CPU 缓存里存储数据的最小的储存单元(好绕口>_<),多个数据可以存储在一个缓存行,一个数据也可以存储在多个缓存行(此种情况会导致 MESI 协议失效)。

MESI 失效之后,系统会自动启用总线加锁机制,从而导致程序的执行效率大打折扣。

4、总线嗅探机制

处理器为了提高处理速度,不直接和主内存相互通讯,而是先将系统内存的数据读取到内部的高速缓存(L1,L2或其他)之后再进行后续操作。处理器和主内存之间是通过总线相互通讯的,通讯过程中,所有的数据都会经过总线,即所有数据将由总线向两端传输。既然数据经过总线传输,那么 CPU 对总线上的数据进行监听便成为可能。

处理器的总线嗅探机制可简单地可理解为:CPU 对总线上的数据的监听。

5、MESI 缓存一致性协议基于总线嗅探机制,结合上面那段加上 volatile 的 Java 源码来思考。大概的,通俗的,可以这样理解:

若使用 volatile 修饰 flag 后,JVM 在运行时会告诉处理器:现在您要按照 MESI 协议上的规范,去保证每个 CPU 缓存里的 flag 这个共享变量副本必须和主内存中的 flag 是一致的(即相等)。若其中一个 CPU 里的线程对共享变量 flag 副本进行了修改,请让这个写线程所在的 CPU 立即启动缓存行加锁机制,并让其马上把修改后的 flag 副本从其自己的缓存里回写到主内存中,缓存行加锁机制启动时,还得请您开始对总线上的数据进行监听(开启总线嗅探机制),当经修改后的 flag 副本的值从 CPU 缓存通过总线回写到主内存时,您将会监听到 flag 回写,而后您告知其他 CPU,当前的共享变量 flag 副本已经无效,让其他 CPU 等待缓存行解锁之后再从主内存里将改变后的 flag 的值重新读取到自己的缓存中原来那个副本所在的存储区域,并将此数据副本所在缓存行标记为S(可简单理解为数据副本被标记为有效)

到此讲了好多理论的东东,讲真,看得累人是吧?!我在贴出汇编指令之前曾说过要用底层的流程图来加以说明,现在可以出图了:

如图,以汇编指令来再次分析,如果对使用 volatile 修饰的共享变量 flag 进行写操作,那么进行这个写操作线程所在的 Core1 将会收到由 JVM 发送的一条 lock 前缀指令修饰的汇编指令:lock addl $0x0,(%rsp),此时,lock 前缀指令会促使处理器(这里是指整个处理器结构)执行 MESI 缓存一致性协议开启总线嗅探机制,同时促使 Core1 启动缓存行锁定机制,在 store 原子操作之前对共享变量 flag 所在的内存区域上锁,然后立即将缓存里的共享变量 flag 副本的值马上回写到主内存里(store --> write)等到  write 原子操作完成之后,解除主内存中共享变量 flag 所在的内存区域的锁定。因此,在 Core1 回写数据期间(即执行 store --> write 原子操作的期间),Core1 独占此区域,Core0 无法从此区域读取共享变量 flag。由于之前 Core1 收到 lock 前缀指令时整个处理器结构已经开启了总线嗅探机制,所以,在 Core1 进行数据回写的同时 Core0 会一直监听着总线的数据,当 Core1 将自己缓存中已改写的 flag 副本通过总线传输到主内存时(这里可简单理解为 store 原子操作),Core0 就会监听到改变的 flag 副本值(即监听到改变的 flag 回写),从而将自己缓存中的 flag 副本失效掉,直到 Core1 回写数据成功,解除了共享内存的锁定,Core0 便再次执行 read --> load 原子操作,重新从主内存读取共享变量 flag 的值,存放到自己缓存中原来那个副本所在的存储区域,并将此数据副本所在缓存行标记为S(可简单理解为数据副本被标记为有效)

可见,volatile 保证了多线程并发运行时每个线程里,用其修饰的共享变量在每个线程里的都是可见的(可见性)。

五、并发编程的三大特性:可见性、原子性、有序性

在此不对这三大特性进行赘述,网络上理论的描述有好多^_^。

上面对使用 volatile 关键字保证多线程中数据的可见性(缓存一致性)已经进行了较为详细的分析。接下来,就该谈谈 volatile 是否保证原子性和有序性了。

看下面代码:

 1 public class TestVolatileAtomic {
 2     private static volatile int num = 0;
 3     
 4     public static void increaseNum() {
 5         num++;
 6     } 
 7     
 8     public static void main(String[] args) {
 9         Thread[] threads = new Thread[2];
10         
11         for (int i = 0; i < threads.length; i++) {
12             threads[i] = new Thread(new Runnable() {
13                 public void run() {
14                     for (int i = 0; i < 1000; i++) {
15                         increaseNum();
16                     }
17                 }
18             });
19             threads[i].start();
20         }
21         
22         for (Thread t : threads) {
23             try {
24                 t.join();
25             } catch (InterruptedException e) {
26                 e.printStackTrace();
27             }
28         }
29         
30         System.out.println(num);  // <= 2000
31     }
32 }

多次运行上面这段代码,你将会发现最后打印出的共享变量 num 的值总是小于等于 2000 的。这又是为什么?还是得看图:

以两个写线程都只执行一次 num++ 为例,最后 num 的值总是 <= 2 的,CPU内部执行程序的机理还是教为复杂的,上图所分析的只是产生 num = 1 这种结果的原因之一,由于能力有限我不再进行更多原因的分析说明。总体来说,可以简单的理解为,volatile 不保证对 num 的操作(这里的操作是指 CPU 从内存读取 num,进行修改后又写回内存的整个操作)是原子操作,因为它只在 store 前上锁,write 后解锁。所以,当有两个及以上的线程对同一个共享变量进行写操作时,这个主存中的共享变量的值总是不确定的。使用 volatile 时需要注意到这个问题。

接下来我们再来看看 volatile 是否保证有序性,先看下面代码:

 

 1 import java.util.*;
 2 
 3 public class TestVolatileSerial {
 4     static int x = 0, y = 0;
 5     public static void main(String[] args) {
 6         Set<String> resultSet = new HashSet<String>();
 7         Map<String, Integer> resultMap = new HashMap<String, Integer>();
 8         
 9         for (int i = 0; i < 1000000; i++) {
10             x = 0; y = 0;
11             resultMap.clear();
12             
13             Thread t0 = new Thread(new Runnable() {
14                 public void run() {
15                     int a = y;
16                     x = 1;
17                     resultMap.put("a", a);
18                 }
19             });
20             
21             Thread t1 = new Thread(new Runnable() {
22                 public void run() {
23                     int b = x;
24                     y = 1;
25                     resultMap.put("b", b);
26                 }
27             });
28             
29             t0.start(); t1.start();
30             try {
31                 t0.join();t1.join();
32             } catch (InterruptedException e) {
33                 e.printStackTrace();
34             } 
35             
36             resultSet.add("a = " + resultMap.get("a") + " " + "b = " + resultMap.get("b"));
37             System.out.println(resultSet);
38         }
39     }
40 }
41 
42 结果中有四种情况:
43 [a = 1 b = 0, a = 1 b = 1, a = 0 b = 0, a = 0 b = 1]

上面让两个线程反复循环执行 100000 次后产生了 4 种结果:a = 1 b = 0,a = 1 b = 1, a = 0 b = 0,a = 0 b = 1。

除了 a = 1 b = 1 这种情况,我们对其他的结果应该都比较好理解,那么为什么会产生 a = 1 b = 1 的这种情况?

简单而言,产生 a 和 b 的值都是 1 的这种情况是因为 CPU 在执行某段程序的时候会进行指令重排。具体以上面代码来讲,若当 CPU 进行指令重排后,实际执行顺序可能如下:

t0中指令重排后:x = 1 --> int a = y。先执行 x = 1,后执行 int a = y;
t1中指令重排后:y = 1 --> int b = x。先执行 y = 1,后执行 int b = x

显而易见,指令重排后导致最后 a 和 b 的值都为 1。

那么使用 volatile 后情况又如何?还是上面那段代码,我们在 x 和 y 前面加上 volatile,然后运行,最后无论你把循环的次数增加到多大,等代码执行结束只会出现三种情况的结果:

[a = 1 b = 0, a = 0 b = 0, a = 0 b = 1]

可见,使用 volatile 修饰 x 和 y 后,禁止了 CPU 对操作 x 和 y 这两个共享变量的指令的重新排序,它保证了数据在并发操作时的有序性。

关于 volatile 为什么保证有序性而不保证原子性的更底层的机理,博主暂时没有能力进行更加深入的分析探讨,不过我将继续溯本追源,弄清原理。

希望此文对爱好编程的朋友有所帮助,在此向所有巨人和先行者致敬!

posted @ 2020-02-08 18:46  Phpythoner  阅读(638)  评论(0编辑  收藏  举报