指令重排序和volatile知识点汇总

2018-02-28 07:48:50来源:作者:人点击

分享
第七城市

当前内容来自互联网整理
Java语言规范对volatile的定义如下:
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量;在JVM底层volatile是采用“内存屏障”来实现的;观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时会多出一个lock前缀指令,lock前缀指令其实就相当于一个内存屏障,内存屏障是一组处理指令,用来实现对内存操作的顺序限制;在JMM中的线程之间的通信采用共享内存来实现的,此处volatile的内存语义是:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中,当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。
volatile相对于synchronized稍微轻量些,在某些场合它可以替代synchronized,但是又不能完全取代synchronized,只有在某些场合才能够使用volatile,使用它必须满足如下两个条件:
(a)对变量的写操作不依赖当前值;
(b)该变量没有包含在具有其他变量的不变式中。
volatile经常用于两个场景:状态标记量,double check。
volatile的特性:
可见性:对volatile的读,总可以看到对这个变量最终的写;
原子性:volatile对单个读/写具有原子性(32位Long,Double),但是复合操作除外,例如i++;
指令重排序:在不改变程序执行结果的前提下,尽可能提高程序的运行效率
(a)编译器重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
(b)处理器重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
JIT在重排序时会在catch语句中插入错误代偿代码,这样做虽然会导致cathc里面的逻辑变得复杂,但是JIT优化原则是:尽可能地优化程序正常运行下的逻辑,哪怕以catch块逻辑变得复杂为代价。重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。注意:X86CPU不支持写写重排序。
指令重排序需要满足以下两个条件:
(a)在单线程环境下不能改变程序运行的结果;
(b)存在数据依赖关系的不允许重排序,无法通过happens-before原则推导出来的,JMM允许任意的排序。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系;Happens-before是用来判断是否存数据竞争、线程是否安全的主要依据,它保证了多线程环境下的可见性,Happens-before原则定义如下:
1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前;
2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行,即A happens-before B不是A一定会在B之前执行,而是A的对B可见;如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
Happens-before原则规则:
(a)程序次序规则:在单独的线程内按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
(b)锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
(c)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
(d)传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
(e)线程启动规则:Thread对象的start()方法先行发生于此线程的每个动作;
(f)线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
(g)线程终结规则:线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行;
(h)对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始;
以上是原生Java满足Happens-before关系的规则,但是可以对它们进行推导出其它满足happens-before的规则:
(a)将一个元素放入一个线程安全的队列的操作Happens-Before从队列中取出这个元素的操作
(b)将一个元素放入一个线程安全容器的操作Happens-Before从容器中取出这个元素的操作
(c)在CountDownLatch上的倒数操作Happens-Before CountDownLatch#await()操作
(d)释放Semaphore许可的操作Happens-Before获得许可操作【待验证】
(e)Future表示的任务的所有操作Happens-Before Future#get()操作
(f)向Executor提交一个Runnable或Callable的操作Happens-Before任务开始执行操作
为了实现volatile的内存语义,JMM会限制重排序,其重排序规则如下:
(a)volatile读之后的操作不会被编译器重排序到volatile读之前;
(b)volatile写之前的操作不会被编译器重排序到volatile写之后;
(c)当首先操作volatile写,接着操作volatile读时,不能重排序。
volatile的底层实现是通过插入内存屏障,但是对于编译器来说,发现最优布置来最小化插入内存屏障的总数几乎是不可能的,所以JMM采用了保守策略:
在每个volatile写操作前面插入StoreStore屏障;
在每个volatile写操作后面插入StoreLoad屏障;
在每个volatile读操作后面插入LoadLoad屏障;
在每个volatile读操作后面插入LoadStore屏障;
StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。
LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

第七城市

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台