【死磕Java并发】-----Java内存模型之重排序

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件: 1. 在单线程环境下不能改变程序运行的结果; 2. 存在数据依赖关系的不允许重排序

如果看过LZ上篇博客的就会知道,其实这两点可以归结于一点:无法通过happens-before原则推导出来的,JMM允许任意的排序。

as-if-serial语义

as-if-serial语义的意思是,所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守as-if-serial语义。注意as-if-serial只保证单线程环境,多线程环境下无效。

下面我们用一个简单的示例来说明:

int a = 1 ;      //Aint b = 2 ;      //Bint c = a + b;   //C

A、B、C三个操作存在如下关系:A、B不存在数据依赖关系,A和C、B和C存在数据依赖关系,因此在进行重排序的时候,A、B可以随意排序,但是必须位于C的前面,执行顺序可以是A –> B –> C或者B –> A –> C。但是无论是何种执行顺序最终的结果C总是等于3。

as-if-serail语义把单线程程序保护起来了,它可以保证在重排序的前提下程序的最终结果始终都是一致的。

其实对于上段代码,他们存在这样的happen-before关系:

  1. A happens-before B
  2. B happens-before C
  3. A happens-before C

1、2是程序顺序次序规则,3是传递性。但是,不是说通过重排序,B可能会排在A之前执行么,为何还会存在存在A happens-beforeB呢?这里再次申明A happens-before B不是A一定会在B之前执行,而是A的对B可见,但是相对于这个程序A的执行结果不需要对B可见,且他们重排序后不会影响结果,所以JMM不会认为这种重排序非法。

我们需要明白这点:在不改变程序执行结果的前提下,尽可能提高程序的运行效率。

下面我们在看一段有意思的代码:

public class RecordExample1 {
    public static void main(String[] args){                    int a = 1;                    int b = 2;                try {
            a = 3;           //A
            b = 1 / 0;       //B
        } catch (Exception e) {

        } finally {
            System.out.println("a = " + a);
        }
    }
}

按照重排序的规则,操作A与操作B有可能会进行重排序,如果重排序了,B会抛出异常( / by zero),此时A语句一定会执行不到,那么a还会等于3么?如果按照as-if-serial原则它就改变了程序的结果。其实JVM对异常做了一种特殊的处理,为了保证as-if-serial语义,Java异常处理机制对重排序做了一种特殊的处理:JIT在重排序时会在catch语句中插入错误代偿代码(a = 3),这样做虽然会导致cathc里面的逻辑变得复杂,但是JIT优化原则是:尽可能地优化程序正常运行下的逻辑,哪怕以catch块逻辑变得复杂为代价。

重排序对多线程的影响

在单线程环境下由于as-if-serial语义,重排序无法影响最终的结果,但是对于多线程环境呢?

如下代码(volatile的经典用法):

public class RecordExample2 {
    int a = 0;    boolean flag = false;       /**
     * A线程执行
     */
    public void writer(){
        a = 1;                  // 1
        flag = true;            // 2
    }       /**
     * B线程执行
     */
    public void read(){        if(flag){                  // 3
           int i = a + a;          // 4
        }
    }

}

A线程执行writer(),线程B执行read(),线程B在执行时能否读到 a = 1 呢?答案是不一定(注:X86CPU不支持写写重排序,如果是在x86上面操作,这个一定会是a=1,LZ搞了好久都没有测试出来,最后查资料才发现)。

由于操作1 和操作2 之间没有数据依赖性,所以可以进行重排序处理,操作3 和操作4 之间也没有数据依赖性,他们亦可以进行重排序,但是操作3 和操作4 之间存在控制依赖性。假如操作1 和操作2 之间重排序:

按照这种执行顺序线程B肯定读不到线程A设置的a值,在这里多线程的语义就已经被重排序破坏了。

操作3 和操作4 之间也可以重排序,这里就不阐述了。但是他们之间存在一个控制依赖的关系,因为只有操作3 成立操作4 才会执行。当代码中存在控制依赖性时,会影响指令序列的执行的并行度,所以编译器和处理器会采用猜测执行来克服控制依赖对并行度的影响。假如操作3 和操作4重排序了,操作4 先执行,则先会把计算结果临时保存到重排序缓冲中,当操作3 为真时才会将计算结果写入变量i中

通过上面的分析,重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义

参考资料

  1. 周志明 :《深入理解Java虚拟机》
  2. 方腾飞:《Java并发编程的艺术》

END

原文发布于微信公众号 - Java技术驿站(chenssy89)

原文发表时间:2018-05-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ImportSource

JVM中的“同步”到底是怎么实现的?

JVM中的Synchronization是使用monitor entry和exit来实现的。不管是显式的还是隐式的。显式的是通过使用monitorenter和m...

2885
来自专栏java、Spring、技术分享

JVM学习笔记

  java引用类型分为四种:类、接口、数组类和泛型参数。其中泛型参数会在编译过程中被擦除。因此 Java 虚拟机实际上只有前三种。在类、接口和数组类中,数组类...

1142
来自专栏芋道源码1024

【死磕Java并发】—– Java内存模型之重排序

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件: 在单线程环境下不能改...

3747
来自专栏JavaQ

高并发编程-synchronized深入解析深挖

Java虚拟机的运行时数据区中的堆和方法区是所有线程共享的区域,如果多个线程需要同时使用共享的对象或类变量,则必须要正确协调它们对数据的访问。否则,程序将具有不...

911
来自专栏程序员互动联盟

【专业技术第十三讲】指针和内存泄露

存在问题: 指针是大家最为头痛的问题,也是程序bug中较难解决的错误,什么情况下会导致内存泄露? 解决方案: 引言 对于任何使用C语言的人,如果问他们C语言...

3558
来自专栏决胜机器学习

设计模式专题(十五) ——组合模式

设计模式专题(十五)——组合模式 (原创内容,转载请注明来源,谢谢) 一、概述 组合模式(Composite)将对象组合成树形结构,以表示部分-整体的层次结构...

39211
来自专栏章鱼的慢慢技术路

Go语言相关练习_选择题(2)

go语言中字符串是UTF-8编码并存储的,它语言不定长的字节,所以它不支持下标操作,因为没一个下标操作代表的是固定长度的字节,所以不能对字符串中某个字符单独赋值...

1052
来自专栏V站

如何提高PHP编程效率,提升程序编译质量(53条)

用单引号代替双引号来包含字符串,这样做会更快一些。因为PHP会在双引号包围的字符串中搜寻变量,单引号则不会,注意:只有echo能这么做,它是一种可以把多个字符串...

1645
来自专栏PHP在线

php日常使用总结

Private(私有): 只能在这个当前类的内部读取、修改。 Protected(受保护):能够在这个类和类的子类中读取和修改。 在方法内部通过 $this...

3708
来自专栏java一日一条

Java反射在JVM的实现

反射使程序代码能够接入装载到JVM中的类的内部信息,允许在编写与执行时,而不是源代码中选定的类协作的代码,是以开发效率换运行效率的一种手段。这使反射成为构建灵活...

2153

扫码关注云+社区

领取腾讯云代金券