👆点击“博文视点Broadview”,获取更多书讯
什么是指令重排序呢?
为了更加直观地理解,笔者还是通过一个案例来说明。
public class MemoryReorderingExample { private static int x=0,y=0; private static int a=0,b=0; public static void main(String[] args) throws InterruptedException { int i=0; while(true){ i++; x=0;y=0; a=0;b=0; Thread t1=new Thread(()->{ a=1; x=b; }); Thread t2=new Thread(()->{ b=1; y=a; }); t1.start(); t2.start(); t1.join(); t2.join(); String result="第"+i+"次("+x+","+y+")"; if(x==0&&y==0){ System.out.println(result); break; } } }}
上面这段程序的逻辑如下:
我们来看一下运行结果:
第136164次(0,0)
读者看到这个结果是不是大吃一惊?在运行了13万次之后,竟然得到一个x=0、y=0的结果。
其实这就是所谓的指令重排序问题,假设上面的代码通过指令重排序之后,变成下面这种结构:
Thread t1=new Thread(()->{ x=b; //指令重排序 a=1;});Thread t2=new Thread(()->{ y=a; //指令重排序 b=1;});
经过重排序之后,如果t1和t2线程同时运行,就会得到x=0、y=0的结果,这个结果从人的视角来看,就有点类似于t1线程中a=1的修改结果对t2线程不可见,同样t2线程中b=1的执行结果对t1线程不可见。
01
什么是指令重排序
指令重排序是指编译器或CPU为了优化程序的执行性能而对指令进行重新排序的一种手段,重排序会带来可见性问题,所以在多线程开发中必须要关注并规避重排序。
从源代码到最终运行的指令,会经过如下两个阶段的重排序。
第一阶段,编译器重排序,就是在编译过程中,编译器根据上下文分析对指令进行重排序,目的是减少CPU和内存的交互,重排序之后尽可能保证CPU从寄存器或缓存行中读取数据。
在前面分析JIT优化中提到的循环表达式外提(Loop Expression Hoisting)就是编译器层面的重排序,从CPU层面来说,避免了处理器每次都去内存中加载stop,减少了处理器和内存的交互开销。
if(!stop){ while(true){ i++; }}
第二阶段,处理器重排序,处理器重排序分为两个部分。
为了帮助读者理解,笔者专门针对并行指令集的原理做一个简单的说明。
什么是并行指令集?
在处理器内核中一般会有多个执行单元,比如算术逻辑单元、位移单元等。在引入并行指令集之前,CPU在每个时钟周期内只能执行单条指令,也就是说只有一个执行单元在工作,其他执行单元处于空闲状态;在引入并行指令集之后,CPU在一个时钟周期内可以同时分配多条指令在不同的执行单元中执行。
那么什么是并行指令集的重排序呢?
如下图所示,假设某一段程序有多条指令,不同指令的执行实现也不同。
图 并行指令集重排序
对于一条从内存中读取数据的指令,CPU的某个执行单元在执行这条指令并等到返回结果之前,按照CPU的执行速度来说它足够处理几百条其他指令,而CPU为了提高执行效率,会根据单元电路的空闲状态和指令能否提前执行的情况进行分析,把那些指令地址顺序靠后的指令提前到读取内存指令之前完成。
实际上,这种优化的本质是通过提前执行其他可执行指令来填补CPU的时间空隙,然后在结束时重新排序运算结果,从而实现指令顺序执行的运行结果。
02
as-if-serial语义
as-if-serial表示所有的程序指令都可以因为优化而被重排序,但是在优化的过程中必须要保证是在单线程环境下,重排序之后的运行结果和程序代码本身预期的执行结果一致,Java编译器、CPU指令重排序都需要保证在单线程环境下的as-if-serial语义是正确的。
可能有些读者会有疑惑,既然能够保证在单线程环境下的顺序性,那为什么还会存在指令重排序呢?在JSR-133规范中,原文是这么说的。
The compiler, runtime, and hardware are supposed to conspire to create the illusion of as-if-serial semantics, which means that in a single-threaded program, the program should not be able to observe the effects of reorderings.However, reorderings can come into play in incorrectly synchronized multithreaded programs, where one thread is able to observe the effects of other threads, and may be able to detect that variable accesses become visible to other threads in a different order than executed or specified in the program.
as-if-serial语义允许重排序,CPU层面的指令优化依然存在。在单线程中,这些优化并不会影响整体的执行结果,在多线程中,重排序会带来可见性问题。
另外,为了保证as-if-serial语义是正确的,编译器和处理器不会对存在依赖关系的操作进行指令重排序,因为这样会影响程序的执行结果。我们来看下面这段代码。
public void execute(){ int x=10; //1 int y=5; //2 int c=x+y; //3}
上述代码按照正常的执行顺序应该是1、2、3,在多线程环境下,可能会出现2、1、3这样的执行顺序,但是一定不会出现3、2、1这样的顺序,因为3与1和2存在数据依赖关系,一旦重排序,就无法保证as-if-serial语义是正确的。
至此,相信读者对指令重排序导致的可见性问题有了一个基本的了解,但是在CPU层面还存在内存系统重排序问题,内存系统重排序也会导致可见性问题,《Java并发编程深度解析与实战》一书还会围绕这个问题做一个详细的分析。
*本文节选自《Java并发编程深度解析与实战》一书,欢迎阅读本书了解更多精彩内容!
▊《Java并发编程深度解析与实战》
谭锋 著
本书涵盖Java并发编程体系的核心库和核心类的使用及原理分析,具体包括线程、synchronized、volatile、J.U.C中的重入锁和读写锁、并发中的条件等待机制、J.U.C并发工具集、深度探索并发编程不得不知的工具、阻塞队列、并发安全集合、线程池、异步编程特性等。书中针对每一个技术点,纵向分析与其相关的所有内容,并且对相关知识点进行了非常详细的说明,同时从架构实践的角度来看待并发,通过大量实战案例让读者理解各类技术在实际应用中的使用方法。
(京东满100减50,快快扫码抢购吧!)
如果喜欢本文欢迎 在看丨留言丨分享至朋友圈 三连
热文推荐
告诉你!操作系统是个大骗子!
来看看深度学习如何在文娱行业“落地”
只看目录就很爽,穿越中国互联网
一文教你用 Python 对 Excel文件进行批量操作
▼点击阅读原文,查看本书详情~
本文分享自 博文视点Broadview 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!