本文暂不讲JMM(Java Memory Model)中的主存, 工做内存以及数据如何在其中流转等等,java
这些自己还牵扯到硬件内存架构, 直接上手容易绕晕, 先从如下几个点探索JMM缓存
原子性是指一个操做是不可中断的. 即便是在多个线程一块儿执行的时候,安全
一个操做一旦开始,就不会被其它线程干扰. 例如CPU中的一些指令, 属于原子性的,多线程
又或者变量直接赋值操做(i = 1), 也是原子性的, 即便有多个线程对i赋值, 相互也不会干扰.架构
而如i++, 则不是原子性的, 由于他实际上i = i + 1, 若存在多个线程操做i, 结果将不可预期.app
有序性是指在单线程环境中, 程序是按序依次执行的.jvm
而在多线程环境中, 程序的执行可能由于指令重排而出现乱序, 下文会有详细讲述.函数
1 class OrderExample { 2 int a = 0; 3 boolean flag = false; 4 5 public void writer() { 6 // 如下两句执行顺序可能会在指令重排等场景下发生变化 7 a = 1; 8 flag = true; 9 } 10 11 public void reader() { 12 if (flag) { 13 int i = a + 1; 14 …… 15 } 16 } 17 }
可见性是指当一个线程修改了某一个共享变量的值,其余线程是否可以当即知道这个修改.oop
会有多种场景影响到可见性:性能
CPU指令重排
多条汇编指令执行时, 考虑性能因素, 会致使执行乱序, 下文会有详细讲述.
硬件优化(如写吸取,批操做)
cpu2修改了变量T, 而cpu1却从高速缓存cache中读取了以前T的副本, 致使数据不一致.
编译器优化
主要是Java虚拟机层面的可见性, 下文会有详细讲述.
指令重排是指在程序执行过程当中, 为了性能考虑, 编译器和CPU可能会对指令从新排序.
一条汇编指令的执行是能够分为不少步骤的, 分为不一样的硬件执行
既然指令能够被分解为不少步骤, 那么多条指令就不必定依次序执行.
由于每次只执行一条指令, 依次执行效率过低了, 假设上述每个步骤都要消耗一个时钟周期,
那么依次执行的话, 一条指令要5个时钟周期, 两条指令要占用10个时钟周期, 三条指令消耗15个时钟.
而若是硬件空闲便可执行下一步, 相似于工厂中的流水线, 一条指令要5个时钟周期,
两条指令只须要6个时钟周期, 由于是错位流水执行, 三条指令消耗7个时钟.
举个例子 A = B + C, 须要以下指令
注意下图红色框选部分, 指令1, 2独立执行, 互不干扰.
指令3依赖于指令1, 2加载结果, 所以红色框选部分表示在等待指令1, 2结束.
待指令1, 2都已经走完MEM部分, 数据加载到内存后, 指令3继续执行计算EX.
同理指令4须要等指令3计算完, 才能够拿到R3, 所以也须要错位等待.
再来看一个复杂的例子
a = b + c
d = e - f
具体指令执行步骤如图, 再也不赘述, 与上图相似, 在执行过程当中一样会出现等待.
这边框选的X统称一个气泡, 有没有什么方案能够削减这类气泡呢.
答案天然是能够的, 咱们能够在出现气泡以前, 执行其余不相干指令来减小气泡.
例如能够将第五步的加载e到寄存器提早执行, 消除第一个气泡,
同理将第六步的加载f到寄存器提早执行, 消除第二个气泡.
通过指令重排后, 整个流水线会更加顺畅, 无气泡阻塞执行.
原先须要14个时钟周期的指令, 重排后, 只须要12个时钟周期便可执行完毕.
指令重排只可能发生在毫无关系的指令之间, 若是指令之间存在依赖关系, 则不会重排.
如 指令1 : a = 1 指令2: b = a - 1, 则指令1, 2 不会发生重排.
主要指jvm层面的, 以下代码, 在jvm client模式很快就跳出了while循环, 而在server模式下运行, 永远不会中止.
1 /** 2 * Created by Administrator on 2018/5/3/0003. 3 */ 4 public class VisibilityTest extends Thread { 5 private boolean stop; 6 7 public void run() { 8 int i = 0; 9 while (!stop) { 10 i++; 11 } 12 System.out.println("finish loop,i=" + i); 13 } 14 15 public void stopIt() { 16 stop = true; 17 } 18 19 public boolean getStop() { 20 return stop; 21 } 22 23 public static void main(String[] args) throws Exception { 24 VisibilityTest v = new VisibilityTest(); 25 v.start(); 26 Thread.sleep(1000); 27 v.stopIt(); 28 Thread.sleep(2000); 29 System.out.println("finish main"); 30 System.out.println(v.getStop()); 31 } 32 }
以32位jdk1.7.0_55为例, 咱们能够经过修改JAVA_HOME/jre/lib/i386/jvm.cfg, 将jvm调整为server模式验证下.
修改内容以下图所示, 将-server调整到-client的上面.
-server KNOWN
-client KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR
修改为功后, java -version会产生如图变化.
二者区别在于当jvm运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器,
而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对完全,
会致使程序启动慢, 但服务起来以后, 性能更高, 同时有可能带来可见性问题.
咱们将上述代码运行的汇编代码打印出来, 打印方法也简单提一下.
给主类运行时加上VM Options, -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
此时会提示Could not load hsdis-i386.dll; library not loadable; PrintAssembly is disabled
由于打印汇编须要给jdk安装一个插件, 可能须要本身编译hsdis, 不一样平台不太同样,
Windows下32位jdk须要的是hsdis-i386.dll, 64位jdk须要hsdis-amd64.dll.
咱们把编译好的hsdis-i386.dll放到JAVA_HOME/jre/bin/server以及JAVA_HOME/jre/bin/client目录中.
运行代码, 控制台会把代码对应的汇编指令一块儿打印出来. 会有不少行, 咱们只须要搜索run方法对应的汇编.
搜索 'run' '()V' in 'VisibilityTest', 能够找到对应的指令.
以下代码所示, 从红字注释的部分能够看出来,
只有第一次进入循环以前, 检查了下stop的值, 不知足条件进入循环后,
再也没有检查stop, 一直在作循环i++.
1 public void run() { 2 int i = 0; 3 while (!stop) { 4 i++; 5 } 6 System.out.println("finish loop,i=" + i); 7 } 8 9 10 # {method} 'run' '()V' in 'VisibilityTest' 11 ...... 12 0x02d486e9: jne 0x02d48715 13 // 获取stop的值 14 0x02d486eb: movzbl 0x64(%ebp),%ecx ; implicit exception: dispatches to 0x02d48703 15 0x02d486ef: test %ecx,%ecx 16 // 进入while以前, 若stop知足条件, 则跳转到0x02d48703, 不执行while循环 17 0x02d486f1: jne 0x02d48703 ;*goto 18 ; - VisibilityTest::run@12 (line 10) 19 // 循环体内, i++ 20 0x02d486f3: inc %edi ; OopMap{ebp=Oop off=52} 21 ;*goto 22 ; - VisibilityTest::run@12 (line 10) 23 0x02d486f4: test %edi,0xe00000 ;*goto 24 ; - VisibilityTest::run@12 (line 10) 25 ; {poll} 26 // jmp, 无条件跳转到0x02d486f3, 一直执行i++操做, 根本不检查stop的值 27 // 致使死循环 28 0x02d486fa: jmp 0x02d486f3 29 0x02d486fc: mov $0x0,%ebp 30 0x02d48701: jmp 0x02d486eb 31 // 跳出循环 32 0x02d48703: mov $0xffffff86,%ecx 33 ......
解决方案也很简单, 只要给stop加上volatile关键字, 再次打印汇编代码, 发现他每次都会检查stop的值.
就不会出现无限循环了.
1 // 给stop加上volatile后 2 public void run() { 3 int i = 0; 4 while (!stop) { 5 i++; 6 } 7 System.out.println("finish loop,i=" + i); 8 } 9 10 # {method} 'run' '()V' in 'VisibilityTest' 11 ...... 12 0x02b4895c: mov 0x4(%ebp),%ecx ; implicit exception: dispatches to 0x02b4899d 13 0x02b4895f: cmp $0x5dd5238,%ecx ; {oop('VisibilityTest')} 14 // 进入while判断 15 0x02b48965: jne 0x02b4898d ;*aload_0 16 ; - VisibilityTest::run@2 (line 9) 17 // 跳转到0x02b48977获取stop 18 0x02b48967: jmp 0x02b48977 19 0x02b48969: nopl 0x0(%eax)
// 循环体内, i++ 20 0x02b48970: inc %ebx ; OopMap{ebp=Oop off=49} 21 ;*goto 22 ; - VisibilityTest::run@12 (line 10) 23 0x02b48971: test %edi,0xb30000 ;*aload_0 24 ; - VisibilityTest::run@2 (line 9) 25 ; {poll} 26 // 循环过程当中获取stop的值 27 0x02b48977: movzbl 0x64(%ebp),%eax ;*getfield stop 28 ; - VisibilityTest::run@3 (line 9) 29 // 验证stop的值 30 0x02b4897b: test %eax,%eax 31 // 若stop不符合条件, 则继续跳转到0x02b48970: inc, 执行i++, 不然中断循环 32 0x02b4897d: je 0x02b48970 ;*ifne 33 ; - VisibilityTest::run@6 (line 9) 34 0x02b4897f: mov $0x33,%ecx 35 0x02b48984: mov %ebx,%ebp 36 0x02b48986: nop 37 // 跳出循环, 执行System.out.print打印 38 0x02b48987: call 0x02b2cac0 ; OopMap{off=76} 39 ;*getstatic out 40 ; - VisibilityTest::run@15 (line 12) 41 ; {runtime_call} 42 0x02b4898c: int3 44 0x02b4898d: mov $0xffffff9d,%ecx 45 ......
再来看两个从Java语言规范中摘取的例子, 也是涉及到编译器优化重排, 这里再也不作详细解释, 只说下结果.
例子1中有可能出现r2 = 2 而且 r1 = 1;
例子2中是r2, r5值由于都是=r1.x, 编译器会使用向前替换, 把r5指向到r2, 最终可能致使r2=r5=0, r4 = 3;
若是光靠sychronized和volatile来保证程序执行过程当中的原子性, 有序性, 可见性, 那么代码将会变得异常繁琐.
JMM提供了Happen-Before规则来约束数据之间是否存在竞争, 线程环境是否安全, 具体以下:
顺序原则
一个线程内保证语义的串行性; a = 1; b = a + 1;
volatile规则
volatile变量的写,先发生于读,这保证了volatile变量的可见性,
锁规则
解锁(unlock)必然发生在随后的加锁(lock)前.
传递性
A先于B,B先于C,那么A必然先于C.
线程启动, 中断, 终止
线程的start()方法先于它的每个动做.
线程的中断(interrupt())先于被中断线程的代码.
线程的全部操做先于线程的终结(Thread.join()).
对象终结
对象的构造函数执行结束先于finalize()方法.