前言
在进入正题之前,读者们可以移步去看看下面三篇文章:
在《反骨之Java是如何解决并发中的可见性问题的》一文中,笔者曾经提到过volatile的第二层语义是:禁止指令重排序和禁止编译器优化。那…到底volatile是利用什么手段,实现禁止指令重排序和禁止编译优化的呢?别着急,好戏马上开始。顺便一提,为了避免一开始就大篇幅的介绍各类知识点,接下来笔者将着重从以下两个方面开始介绍:
- 什么是重排序以及重排序的类型都有些什么?
- Java并发编程中是如何解决有序性问题的?
拾贝语录: 其实,相对论和有序性是有关联的。在`有序性`问题中,主要是指一个处理器CPU上运行一个线程A所执行的内存操作,在另外一个处理器CPU上运行的线程B看来是乱序的。在这里主要强调:对于线程A而言其代码执行是按序执行的,但站在线程B的视角上观察线程A,那么线程A的执行顺序却改变了。
正文
重排序的概念
在各类高级语言编程中(c++/Java..),源代码是需要通过编译器将高级语言翻译成机器语言,才能够在计算机上运行。众所周知,在Java语言中所有的源代码经过编译器编译后并不直接运行在计算机中,而是运行在Java自带的Jvm(Java Virtual Machine)运行环境。那么,在JVM环境中源代码(.class)的执行顺序与程序的执行顺序(runtime)不一致,或者程序执行顺序与执行顺序不一致的情况下,我们就称程序执行过程中发生了重排序。
在代码顺序结构中,我们可以直观的指定代码的执行顺序, 即从上到下按序执行。但是,对于编译器和CPU处理器两个层面来说,执行顺序可就由不得你了。因为编译器和CPU会根据自己的决策,对代码的执行顺序进行重新排序。在这里笔者要强调,重排序对程序的性能和执行速度是有好处的,重排序是编译器和CPU在内存访问操作上所做的一种优化手段。这种优化手段在单线程环境下并不影响程序输出结果的正确性,但是在多线程环境下有可能会对程序输出结果产生影响。
拾贝语录: 事物有两面性,好心也有可能做坏事。重排序的目的是对代码执行顺序进行优化,以便于程序在运行的时候,能够有更好的执行速度和效率。但是在多线程环境下,这种重排序优化手段往往会发生不可预料的问题。
重排序的类型
在笔者以前的认知过程中,发生重排序一般是在编译器中。但是,真正发生的重排序的潜在原因有多个,包括但不止于编译器。比如还有CPU处理器、存储系统都会产生重排序。根据由什么原因产生的重排序,我们将重排序分为两大类:指令重排序和内存访问指令重排序。
指令重排序一般泛指编译器/编译软件对源代码(.java/.cpp等)的执行顺序,进行适当的重新排序以后让其更适合CPU的执行特性。而内存访问重排序一般强调CPU在执行过程中,会动态地分析硬件的效能,对内存的读写顺序做出修改,而这些修改则会造成内存访问指令进行重排序。
重排序解决方案:HB原则(happens-before)
前方高冷预警: 关于HB相关的知识点笔者自己都有些糊涂,所以关于HB相关的知识点这块,如果写得不好读者们可以完全跳过。
刚接触HB原则的时候,笔者的直观感觉就是:A操作先行发生于B操作, 所以就叫先行发生原则。在踩坑期间笔者翻阅过较多相关技术书籍和博客,发现一旦讲到HB原则,各种神仙博主一言不合就抛出HB的几大核心内容:程序顺序原则、锁原则、valotile原则、传递性原则、线程启动原则、线程中断原则、线程终结原则、对象创建原则。不管HB包含多少个非官方解释多少个核心原则,我们只需要记住HB原则的唯一目的, 即:HB原则并不意味着前一个操作必须要比后一个操作先执行,而仅仅是为了保证前一个操作的结果是对后续的操作是立即可见的。
拾贝语录: 我是革命的一块砖,哪里需要往哪里搬。在JMM规范中,HB规则是用来保证可见性的, 即如果一个操作结果在内存上对另外一个操作是可见的,那么这两个操作之间必须符合HB原则。所以,广义上来讲HB原则是保证可见性的强力手段,而HB原则中的某些规则具有保证有序性的功能。
重排序解决方案:内存屏障
在笔者将内存屏障之前,不得不提之前写的一篇文章《反骨之硬件&软件为Java并发编程中挖的坑(可见性&原子性&有序性)》这篇文章一个观点:Cpu多核缓存给并发编程带来可见性问题。那么内存屏障的使用条件就是存在多核缓存,要知道在现代处理器中为了保证指令的流水线执行,一般会在处理器中增加一块读/写高速缓存, 但是这里会出现一个大问题,即每一块读/写高速缓存仅仅对所属处理器可见,这个特性在多线程环境下会对内存操作的执行顺序产生影响:处理器对高速缓存中的读写顺序,不一定在主内存中实际发生。
此时使用内存屏障就发挥了极大的作用,那么是什么内存屏障?内存屏障就是让一个Cpu缓存中的状态(变量)对其他Cpu缓存可见的一种技术。内存屏障是在硬件层进行实现的,而内存屏障是一种标准,各硬件厂商可能会采用不同的实现。
结束语
-
重排序分为:指令重排序和内存访问指令重排序
-
HB原则:程序顺序原则、锁原则、valotile原则、传递性原则、线程启动原则、线程中断原则、线程终结原则、对象创建原则
-
内存屏障:硬件(CPU)指令。它使得CPU 或编译器在对内存进行操作的时候, 严格按照一定的顺序来执行。
-
解决有序性问题:HB原则+内存屏障