我是否是学了一门假的java。。。。。。html
引言:在Java中看似顺序的代码在JVM中,可能会出现编译器或者CPU对这些操做指令进行了从新排序;在特定状况下,指令重排将会给咱们的程序带来不肯定的结果.....java
(带来个毛的不肯定,他奶奶的多线程只存在于学习Java基础,实际工做中用的不多,除非是本身造轮子;因此我写这个算不算咸吃萝卜淡操心捏?)程序员
本文大部分来自于:Java内存访问重排序的研究,想看原做请移步。编程
以下代码可能的结果有哪些?缓存
public class PossibleReordering { static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start();other.start(); one.join();other.join(); System.out.println("(" + x + "," + y + ")"); }
}
看完本文你就明白了。(明白了也没啥卵用,别看了)性能优化
(看这个是否是感受很牛逼的词,感受是否是要学习一下编译原理,反正看了以后发现发现学习编译原理不只仅在于去开发一门编译器,还在于对语言的深度学习啊)多线程
在计算机执行指令的顺序在通过程序编译器编译以后造成的指令序列,通常而言,这个指令序列是会输出肯定的结果;以确保每一次的执行都有肯定的结果。可是,通常状况下,CPU和编译器为了提高程序执行的效率,会按照必定的规则容许进行指令优化。架构
为鸡毛重排能够提升代码的执行效率?并发
大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件容许的状况下,直接运行当前有能力当即执行的后续指令,避开获取下一条指令所需数据时形成的等待。经过乱序执行的技术,处理器能够大大提升执行效率。
除了处理器,常见的Java运行时环境的JIT编译器也会作指令重排序操做,即生成的机器指令与字节码指令顺序不一致。oracle
(CPU要的指令重排是否是从另外一方面告诫咱们写代码的方向呢,不只处理器欺负咱们,连javac都欺负咱们,555,不能好好开发了)
在某些状况下,这种优化会带来一些执行的逻辑问题,主要的缘由是代码逻辑之间是存在必定的前后顺序,在并发执行状况下,会发生二义性,即按照不一样的执行逻辑,会获得不一样的结果信息。
主要指不一样的程序指令之间的顺序是不容许进行交换的,便可称这些程序指令之间存在数据依赖性。
哪些指令不容许重排?
主要的例子以下:
名称 代码示例 说明
写后读 a = 1;b = a; 写一个变量以后,再读这个位置。
写后写 a = 1;a = 2; 写一个变量以后,再写这个变量。
读后写 a = b;b = 1; 读一个变量以后,再写这个变量。
进过度析,发现这里每组指令中都有写操做,这个写操做的位置是不容许变化的,不然将带来不同的执行结果。
编译器将不会对存在数据依赖性的程序指令进行重排,这里的依赖性仅仅指单线程状况下的数据依赖性;多线程并发状况下,此规则将失效。
无论怎么重排序(编译器和处理器为了提升并行度),单线程程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵照as-if-serial语义。
分析: 关键词是单线程状况下,必须遵照;其他的不遵照。
as-if-serial语义是啥?
as-if-serial语义的意思是,全部的动做(Action)均可觉得了优化而被重排序,可是必须保证它们重排序后的结果和程序代码自己的应有结果是一致的。Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义。
好比,为了保证这一语义,重排序不会发生在有数据依赖的操做之中。
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C
分析代码:
A->C B->C; A,B之间不存在依赖关系; 故在单线程状况下, A与B的指令顺序是能够重排的,C不容许重排,必须在A和B以后。
as-if-serial语义把单线程程序保护了起来,遵照as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员建立了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担忧重排序会干扰他们,也无需担忧内存可见性问题。核心点仍是单线程,多线程状况下不遵照此原则。
从这里开始,不少都是拷贝的,水平有限,拷贝来补,逃~~~
计算机系统中,为了尽量地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提升性能。其模型以下图所示。
在这种模型下会存在一个现象,即缓存中的数据与主内存的数据并非实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的。这致使在同一个时间点,各CPU所看到同一内存地址的数据的值多是不一致的。从程序的视角来看,就是在同一个时间点,各个线程所看到的共享变量的值多是不一致的。
有的观点会将这种现象也视为重排序的一种,命名为"内存系统重排序"。由于这种内存可见性问题形成的结果就好像是内存访问指令发生了重排序同样。
这种内存可见性问题也会致使文章最开头示例代码即使在没有发生指令重排序的状况下的执行结果也仍是(0, 0)。
Java的目标是成为一门平台无关性的语言,即Write once, run anywhere。可是不一样硬件环境下指令重排序的规则不尽相同。例如,x86下运行正常的Java程序在IA64下就可能获得非预期的运行结果。为此,JSR-1337制定了Java内存模型(Java Memory Model, JMM),旨在提供一个统一的可参考的规范,屏蔽平台差别性。从Java 5开始,Java内存模型成为Java语言规范的一部分。
根据Java内存模型中的规定,能够总结出如下几条happens-before规则。Happens-before的先后两个操做不会被重排序且后者对前者的内存可见。
Happens-before关系只是对Java内存模型的一种近似性的描述,它并不够严谨,但便于平常程序开发参考使用,关于更严谨的Java内存模型的定义和描述,请阅读JSR-133原文或Java语言规范章节17.4。
除此以外,Java内存模型对volatile和final的语义作了扩展。对volatile语义的扩展保证了volatile变量在一些状况下不会重排序,volatile的64位变量double和long的读取和赋值操做都是原子的。对final语义的扩展保证一个对象的构建方法结束前,全部final成员变量都必须完成初始化(前提是没有this引用溢出,这里不该该用溢出,而是逸出,呵呵)。
Java内存模型关于重排序的规定,总结后以下表所示。
表中"第二项操做"的含义是指,第一项操做以后的全部指定操做。如,普通读不能与其以后的全部volatile写重排序。另外,JMM也规定了上述volatile和同步块的规则尽适用于存在多线程访问的情景。例如,若编译器(这里的编译器也包括JIT,下同)证实了一个volatile变量只能被单线程访问,那么就可能会把它作为普通变量来处理。
留白的单元格表明容许在不违反Java基本语义的状况下重排序。例如,编译器不会对对同一内存地址的读和写操做重排序,可是容许对不一样地址的读和写操做重排序。
除此以外,为了保证final的新增语义。JSR-133对于final变量的重排序也作了限制。
首先咱们基于一段代码的示例来分析,在多线程状况下,重排是否有不一样结果信息:
class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } public void reader() { if (flag) { // 3 int i = a * a; // 4 } } }
上述的代码,在单线程状况下,执行结果是肯定的, flag=true将被reader的方法体中看到,并正确的设置结果。 可是在多线程状况下,是否仍是只有一个肯定的结果呢?
假设有A和B两个线程同时来执行这个代码片断, 两个可能的执行流程以下:
可能的流程1, 因为1和2语句之间没有数据依赖关系,故二者能够重排,在两个线程之间的可能顺序以下:
可能的流程2:, 在两个线程之间的语句执行顺序以下:
根据happens-before的程序顺序规则,上面计算圆的面积的示例代码存在三个happens-before关系:
啥是happens-before关系?
A happens-before B;
B happens-before C;
A happens-before C;
这里的第3个happens- before关系,是根据happens-before的传递性推导出来的
啥是控制依赖关系?
啥是猜想(Speculation)?
在程序中,操做3和操做4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜想(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜想执行为例,执行线程B的处理器能够提早读取并计算a*a,而后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操做3的条件判断为真时,就把该计算结果写入变量i中。从图中咱们能够看出,猜想执行实质上对操做3和4作了重排序。重排序在这里破坏了多线程程序的语义。
与上面的例子相似的有:
在线程A中:
context = loadContext(); inited = true;
while(!inited ){ //根据线程A中对inited变量的修改决定是否使用context变量 sleep(100); } doSomethingwithconfig(context);
inited = true; context = loadContext();
重排致使双重锁定的单例模式失效的例子
例子2:指令重排致使单例模式失效
咱们都知道一个经典的懒加载方式的双重判断单例模式:
public class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance == null) { synchronzied(Singleton.class) { if(instance == null) { instance = new Singleton(); //非原子操做 } } } return instance; } }
memory =allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance =memory; //3:设置instance指向刚分配的内存地址
memory =allocate(); //1:分配对象的内存空间 instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化 ctorInstance(memory); //2:初始化对象
解决方案:例子1中的inited和例子2中的instance以关键字volatile修饰以后,就会阻止JVM对其相关代码进行指令重排,这样就可以按照既定的顺序指执行。
核心点是:两个线程之间在执行同一段代码之间的critical area,在不一样的线程之间共享变量;因为执行顺序、CPU编译器对于程序指令的优化等形成了不肯定的执行结果。
我感受这种状况大多发生多线程环境下:在你先去判断,而后决定作出操做时容易出错,对你要判断的对象加个volatile是个不错的选择。
volatile关键字能够保证变量的可见性,由于对volatile的操做都在Main Memory中,而Main Memory是被全部线程所共享的,这里的代价就是牺牲了性能,没法利用寄存器或Cache,由于它们都不是全局的,没法保证可见性,可能产生脏读。
volatile还有一个做用就是局部阻止重排序的发生(在JDK1.5以后,可使用volatile变量禁止指令重排序),对volatile变量的操做指令都不会被重排序,由于若是重排序,又可能产生可见性问题。
在保证可见性方面,锁(包括显式锁、对象锁)以及对原子变量的读写均可以确保变量的可见性。
可是实现方式略有不一样,例如同步锁保证获得锁时从内存里从新读入数据刷新缓存,释放锁时将数据写回内存以保数据可见,而volatile变量干脆都是读写内存。
volatile关键字经过提供"内存屏障"的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
大多数的处理器都支持内存屏障的指令。
对于编译器来讲,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采起保守策略。下面是基于保守策略的JMM内存屏障插入策略:
在每一个volatile写操做的前面插入一个StoreStore屏障。
在每一个volatile写操做的后面插入一个StoreLoad屏障。
在每一个volatile读操做的后面插入一个LoadLoad屏障。
在每一个volatile读操做的后面插入一个LoadStore屏障。
内存屏障(Memory Barrier,或有时叫作内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
内存屏障能够被分为如下几种类型
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操做要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操做执行前,保证Store1的写入操做对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操做被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续全部读取操做执行前,保证Store1的写入对全部处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
有的处理器的重排序规则较严,无需内存屏障也能很好的工做,Java编译器会在这种状况下不放置内存屏障。
为了实现前面讨论的JSR-133的规定,Java编译器会这样使用内存屏障。
为了保证final字段的特殊语义,也会在下面的语句加入内存屏障。
x.finalField = v; StoreStore; sharedRef = x;
Intel 64和IA-32是咱们较经常使用的硬件环境,相对于其它处理器而言,它们拥有一种较严格的重排序规则。Pentium 4之后的Intel 64或IA-32处理的重排序规则以下。
在单CPU系统中
在多处理器系统中
值得注意的是,对于Java编译器而言,Intel 64/IA-32架构下处理器不须要LoadLoad、LoadStore、StoreStore屏障,由于不会发生须要这三种屏障的重排序。
如今有这样一个场景,一个容器能够放一个东西,容器支持create方法来建立一个新的东西并放到容器里,支持get方法取到这个容器里的东西。咱们能够较容易地写出下面的代码。
public class Container { public static class SomeThing { private int status; public SomeThing() { status = 1; } public int getStatus() { return status; } } private SomeThing object; public void create() { object = new SomeThing(); } public SomeThing get() { while (object == null) { Thread.yield(); //不加这句话可能会在此出现无限循环 } return object; } }
在单线程场景下,这段代码执行起来是没有问题的。可是在多线程并发场景下,由不一样的线程create和get东西,这段代码是有问题的。问题的缘由与普通的双重检查锁定单例模式(Double Checked Locking, DCL)10相似,即SomeThing的构建与将指向构建中的SomeThing引用赋值到object变量这二者可能会发生重排序。致使get中返回一个正被构建中的不完整的SomeThing对象实例。为了解决这一问题,一般的办法是使用volatile修饰object字段。这种方法避免了重排序,保证了内存可见性,摒弃比使用同步块致使的性能损失更小。可是,假如使用场景对object的内存可见性并不敏感的话(不要求一个线程写入了object,object的新值当即对下一个读取的线程可见),在Intel 64/IA-32环境下,有更好的解决方案。
根据上一章的内容,咱们知道Intel 64/IA-32下写操做之间不会发生重排序,即在处理器中,构建SomeThing对象与赋值到object这两个操做之间的顺序性是能够保证的。这样看起来,仅仅使用volatile来避免重排序是画蛇添足的。可是,Java编译器却可能生成重排序后的指令。但使人高兴的是,Oracle的JDK中提供了Unsafe. putOrderedObject,Unsafe. putOrderedInt,Unsafe. putOrderedLong这三个方法,JDK会在执行这三个方法时插入StoreStore内存屏障,避免发生写操做重排序。而在Intel 64/IA-32架构下,StoreStore屏障并不须要,Java编译器会将StoreStore屏障去除。比起写入volatile变量以后执行StoreLoad屏障的巨大开销,采用这种方法除了避免重排序而带来的性能损失之外,不会带来其它的性能开销。
咱们将作一个小实验来比较两者的性能差别。一种是使用volatile修饰object成员变量。
public class Container { public static class SomeThing { private int status; public SomeThing() { status = 1; } public int getStatus() { return status; } } private volatile SomeThing object; public void create() { object = new SomeThing(); } public SomeThing get() { while (object == null) { Thread.yield(); //不加这句话可能会在此出现无限循环 } return object; } }
一种是利用Unsafe. putOrderedObject在避免在适当的位置发生重排序。
public class Container { public static class SomeThing { private int status; public SomeThing() { status = 1; } public int getStatus() { return status; } } private SomeThing object; private Object value; private static final Unsafe unsafe = getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset(Container.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } public void create() { SomeThing temp = new SomeThing(); unsafe.putOrderedObject(this, valueOffset, null); //将value赋null值只是一项无用操做,实际利用的是这条语句的内存屏障 object = temp; } public SomeThing get() { while (object == null) { Thread.yield(); } return object; } public static Unsafe getUnsafe() { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); return (Unsafe)f.get(null); } catch (Exception e) { } return null; } }
因为直接调用Unsafe.getUnsafe()须要配置JRE获取较高权限,咱们利用反射获取Unsafe中的theUnsafe来取得Unsafe的可用实例。
unsafe.putOrderedObject(this, valueOffset, null)
这句仅仅是为了借用这句话功能的防止写重排序,除此以外无其它做用。
利用下面的代码分别测试两种方案的实际运行时间。在运行时开启-server和 -XX:CompileThreshold=1以模拟生产环境下长时间运行后的JIT优化效果。
public static void main(String[] args) throws InterruptedException { final int THREADS_COUNT = 20; final int LOOP_COUNT = 100000; long sum = 0; long min = Integer.MAX_VALUE; long max = 0; for(int n = 0;n <= 100;n++) { final Container basket = new Container(); List<Thread> putThreads = new ArrayList<Thread>(); List<Thread> takeThreads = new ArrayList<Thread>(); for (int i = 0; i < THREADS_COUNT; i++) { putThreads.add(new Thread() { @Override public void run() { for (int j = 0; j < LOOP_COUNT; j++) { basket.create(); } } }); takeThreads.add(new Thread() { @Override public void run() { for (int j = 0; j < LOOP_COUNT; j++) { basket.get().getStatus(); } } }); } long start = System.nanoTime(); for (int i = 0; i < THREADS_COUNT; i++) { takeThreads.get(i).start(); putThreads.get(i).start(); } for (int i = 0; i < THREADS_COUNT; i++) { takeThreads.get(i).join(); putThreads.get(i).join(); } long end = System.nanoTime(); long period = end - start; if(n == 0) { continue; //因为JIT的编译,第一次执行须要更多时间,将此时间不计入统计 } sum += (period); System.out.println(period); if(period < min) { min = period; } if(period > max) { max = period; } } System.out.println("Average : " + sum / 100); System.out.println("Max : " + max); System.out.println("Min : " + min); }
在笔者的计算机上运行测试,采用volatile方案的运行结果以下
Average : 62535770
Max : 82515000
Min : 45161000
采用unsafe.putOrderedObject方案的运行结果以下
Average : 50746230
Max : 68999000
Min : 38038000
从结果看出,unsafe.putOrderedObject方案比volatile方案平均耗时减小18.9%,最大耗时减小16.4%,最小耗时减小15.8%.另外,即便在其它会发生写写重排序的处理器中,因为StoreStore屏障的性能损耗小于StoreLoad屏障,采用这一方法也是一种可行的方案。但值得再次注意的是,这一方案不是对volatile语义的等价替换,而是在特定场景下作的特殊优化,它仅避免了写写重排序,但不保证内存可见性。
参考文献
https://tech.meituan.com/java-memory-reordering.html
http://en.wikipedia.org/wiki/Out-of-order_execution
Oracle Java Hotspot https://wikis.oracle.com/display/HotSpotInternals/PerformanceTacticIndex IBM JVM http://publib.boulder.ibm.com/infocenter/javasdk/v1r4m2/index.jsp?topic=%2Fcom.ibm.java.doc.diagnostics.142j9%2Fhtml%2Fhowjitopt.html
Java语言规范中对“动做”这个词有一个明确而具体的定义,详见http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.2。
https://community.oracle.com/thread/1544959
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf
参见《Java并发编程实践》章节16.1
Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C): System Programming Guide章节8.2
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
抄得太多,消化不良