为什么会有重新排序?它与happens-before有什么关系?inttwo=b;a=2;}应该不难看出,在上面的例子中,我定义了两个共享变量a和b,以及两个方法。第一种方法是将局部变量one赋值给a,然后将b的值设置为1。第二种方法是将局部变量two赋值给b,然后将a的值设置为2。所以我在这里有一个问题,(one,two)的值是多少?你可能不假思索地告诉我,是(0,1)还是(2,0),就看我的mainmethodmethod方法中先执行哪个了。是的,如果程序运行在单线程上,这样回答是没有错的。但是,如果是在多线程环境下呢?假设methodOne和methodTwo在两个不同的线程上执行。这时候Java虚拟机在执行完任意一个方法的第一条赋值语句后就切换线程。这个(one,two)的值在那个时候可能是(0,0)。看到这里,还有疑问吗?为什么?为什么,我写的程序没问题。说到Java虚拟机,对我来说就变了。?因为在执行的过程中,发生了重排序。它可能是由即时编译器重新排序,可能是由处理器乱序执行,也可能是由内存系统重新排序。简而言之,在程序执行过程中,会发生重新排序,结果可能是(0,0)。为什么会重排序看完上面的你可能会有疑问,为什么会出现重排序呢?我的程序是按照自己的逻辑写的,没问题。Java虚拟机为什么要动我的程序逻辑?想想看,CPU、内存都是非常宝贵的资源。如果Java虚拟机重新排序后没有效果,肯定不会做这种吃力不讨好的事情。那么,重新排序有什么好处呢?重新排序可以提高程序的性能。为了便于理解,我举一个生活中的场景来举例。当你早上醒来时,你会穿衣、洗漱、做饭和吃饭,对吗?那么你醒来之后做什么呢?是不是边洗边先做饭(比如让蒸蛋器给你蒸一个鸡蛋),然后洗完了就可以直接去吃早餐了。你为什么做这个?这不仅仅是为了节省时间和多睡一分钟,对吧?同理,Java虚拟机之所以需要重新排序,也是为了提高程序的性能。您编写的程序就像一行代码一样简单。它可能需要在底层使用不同的硬件。例如,一条指令需要同时使用CPU和打印机设备。但此时CPU的任务已经完成,而打印机的任务还没有完成。该怎么办?不让CPU执行下一条指令?CPU的时间这么宝贵,你不让它工作,你确定不是在浪费它的生命吗?所以为了提高程序的利用率和性能,Java虚拟机在你的指令完全执行完之前,再执行另一条指令。这就是流水线技术最怕的东西?我正在执行命令,正在执行命令,突然中断,恢复中断的成本非常高,所以我们要尽量避免中断的发生。.即时编译器的重新排序、处理器的乱序执行、内存系统的重新排序都是为了减少中断而存在的。至此,你对Java虚拟机的重排序有了了解了吗?重新排序导致的问题回到文章开头的例子。重新排序可提高CPU利用率。是的,它提高了程序性能。没错,但是我的程序得到的结果可能是错误的。这是不是有点亏了?因为重排序可以保证串行语义一致,但是多线程之间没有义务保证语义一致。有解决办法,如果没有,那就再想想。它是如何解决的?这个需要说一下,sequentialconsistencymemorymodelandJMM(JavaMemoryModel,Java内存模型)sequentialconsistencymemorymodelandJMM要说数据一致性就不得不说。数据竞赛。什么是数据竞赛?Java内存模型规范中是这样定义的:在一个线程中写入一个变量,在另一个线程中读取同一个变量。写入和读取不按同步排序。当代码中包含数据竞争时,程序的执行结果往往超出你的想象。比如我们刚开始提到的例子,结果可能是(0,0)。但是多线程程序如果能正确同步,就不会出现上面的结果。Java内存模型对正确同步的多线程程序的内存一致性做出如下保证:如果程序正确同步,程序的执行也会有顺序一致性,即程序的执行结果是相同的与顺序一致性模型中程序的执行结果是一样的。这里的同步包括使用volatile、final、synchronized等关键字来实现多线程下的同步。也就是说,如果这些同步使用不当,JMM将没有内存可见性保证,从而导致编写的程序出错。顺序一致性内存模型是理想状态下的理论参考模型。它为程序员提供了特别强大的内存可见性保证。顺序一致性模型有两个特点:一个线程中的所有操作必须按照程序执行的顺序执行(即按照代码编写的顺序执行)程序是否同步,所有线程只看到执行操作的单一顺序。也就是说,在顺序一致性模型中,每个操作都必须是原子的,并且立即对所有线程可见。上文提到,顺序一致的内存模型是一个理想的理论参考模型,因为顺序一致的内存模型要求操作对所有线程可见,但这会降低Java虚拟机的性能。JMM基于顺序一致性内存模型,做了一些优化:对于同步多线程程序,也就是临界区的代码,JMM允许重新排序(但不允许临界区的代码“逃逸”)")"在临界区之外,因为如果允许的话,会违反锁的内存语义)对于非同步的多线程程序,JMM只提供最低限度的安全性:线程读取的值,或者前一个线程写入的值,或者defaultvalue,不会凭空产生,应该能感觉到,相对于顺序一致性内存模型,JMM给了编译器和处理器一些空间,让他们重新排序。这时候就有一个冲突点:程序员需要JMM提供一个强大的内存模型来写代码,也就是我的代码写的顺序是怎样的,程序执行起来会是什么样子;但是编译器和处理处理器需要JMM对它们的约束越少越好,这样它们才能尽可能的优化以提高性能。JMM作为中介,不仅要满足程序员的需求,还要满足编译器的需求和处理器的需求,那么我们就需要在两者之间找到一个平衡点,让程序员写的代码能够产生结果他期望,同时,让编译器和处理器能够优化JMM提出的解决方案,解决方案是:对于程序员,提供happens-before规则,满足程序员的需求--->简单易行理解,并提供足够强的内存可见性保证;对于编译器和处理器来说,只要不改变程序的执行结果(前提是多线程程序正确同步),你想怎么优化就怎么优化。happens-before最后变成happens-before。先看happens-before关系的定义:如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见。如果两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须按照happens-before关系规定的顺序执行。如果重排序后的执行结果与按照happens-before关系执行的结果相同,那么JMM也允许这样的重排序。看这里。你认为这与as-if-serial语义相同吗?是的,happens-before关系本质上与as-if-serial语义相同。as-if-serial语义保证单线程重排序后的执行结果与程序代码本身应该出现的结果一致,happens-before关系保证正确同步的多线程程序的执行结果不会重新排序。排序改变了。总结一句话:如果操作Ahappens-before操作B,那么操作A对内存的操作对于操作B是可见的,不管它们是否在同一个线程中。在Java中,对于happens-before关系,有如下规定:程序顺序规则:一个线程中的每一个操作,happens-before线程中的任何后续操作监控锁规则:解锁一个锁,happens-before在后续的加锁之前这个volatile变量规则:对一个volatile字段的写入,发生在之前,并且对该volatile字段的任何后续读取都是可传递的:如果Ahappens-beforeB,并且Bhappens-beforeC,则Ahappens-beforeC开始规则:如果线程A执行操作ThreadB。start()启动线程B,然后是ThreadB的线程A。start()操作先于线程B中的任何操作加入规则:如果线程A执行操作ThreadB。join()并成功返回,然后线程B中的任何操作happens-before从线程B到线程A。join()操作成功返回。写到这里,感觉这篇文章终于写完了,从whyreordering到happens-before。参考:《Java 并发编程的艺术》
