在讲重排序之前,先来看一个例子:
package com.cyblogs.thread;
import java.util.HashSet;
import java.util.Set;
/**
* Created with leetcode-cn
*
* @Description: 验证重排序代码
* @Author: chenyuan
* @Date: 2021/3/26
* @Time: 15:05
*/
public class VolatileSerialCase {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
// 用set来保存数据,保证不会重复
Set<String> resultSet = new HashSet<String>();
for (int i = 0; i < 10000000; i++) {
x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(() -> {
a = y;
x = 1;
});
Thread two = new Thread(() -> {
b = x;
y = 1;
});
one.start();
two.start();
one.join();
two.join();
// 等待2个线程都跑完了再把结果添加到Set中去
resultSet.add("a=" + a + ",b=" + b);
System.out.println(resultSet);
}
}
}
上面一段代码是非常经典来讲CPU对指令重排序的案例。因为我们经过一段时间的Run出的结果很惊讶:
[a=0,b=0, a=1,b=0, a=0,b=1, a=1,b=1]
对于a=1,b=1
的出现,是会让人非常的奇怪的。出现这个情况,那代码执行的顺序可能是:
Thread one = new Thread(() -> {
a = y; // 第3步
x = 1; // 第1步
});
Thread two = new Thread(() -> {
b = x; // 第4步
y = 1; // 第2步
});
// 也就是说,在2个线程中,都出现了下面的代码执行到了上面的代码前面去了。
如果是这样子的话,那我们还敢写多线程的代码吗?如果没有一定的规范与约定,那肯定是没人可以写好代码。
其实这些约定都是在JSR-133内存模型与线程规范
里面,它就像是Java的产品需求文档或者说明书。
http://static.cyblogs.com/Jietu20210327-174611.jpg
百度云盘:链接: https://pan.baidu.com/s/1cO5d95Za8lyz8dMaN0i9lA 密码: l08w ,大家可以去下载查阅,这些都比较底层,并不能几句话,几篇文章可以讲清楚。
看完上面,你可能会有疑问,为什么会有重排序呢?
我的程序按照我自己的逻辑写下来好好的没啥问题, Java 虚拟机为什么动我的程序逻辑?
你想想 CPU
、内存这些都是非常宝贵的资源, Java 虚拟机如果在重排序之后没啥效果,肯定也不会做这种费力不讨好的事情。
Java 虚拟机之所以要进行重排序就是为了提高程序的性能。你写的程序,简简单单一行代码,到底层可能需要使用不同的硬件,比如一个指令需要同时使用 CPU
和打印机设备,但是此时 CPU 的任务完成了,打印机的任务还没完成,这个时候怎么办呢? 不让 CPU 执行接下来的指令吗? CPU 的时间那么宝贵,你不让它工作,确定不是在浪费它的生命?
重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如下图所示:
http://static.cyblogs.com/20180326170243607.png
这些重排序可能会导致多线程程序出现内存可见性问题。在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial
语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
回到文章刚开始举的那个例子,重排序提高了 CPU 的利用率没错,提高了程序性能没错,但是我的程序得到的结果可能是错误的啊,这是不是就有点儿得不偿失了?
因为重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致
凡是问题,都有办法解决,要是没有,那就再想想。
它是怎么解决的呢? 这就需要来说说,顺序一致性内存模型和 JMM (Java Memory Model , Java 内存模型)
我们知道Java线程的所有操作都是在工作区进行的,那么工作区和主存之间的变量是怎么进行交互的呢,可以用下面的图来表示。
Java通过几种原子操作完成工作区内存和主存的交互
read
操作传过来的变量值储存到工作区内存的变量副本中。store
操作传过来的值赋值给主存变量。as-if-serial
语义的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime
和处理器都必须遵守as-if-serial
语义。
为了遵守as-if-serial
语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。as-if-serial
语义把单线程程序保护了起来,as-if-serial
语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
终于讲到了 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
关系保证的是正确同步的多线程程序的执行结果不会被重排序改变。
一句话来总结就是:如果操作 A happens-before
操作 B ,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。
在 Java
中,对于 happens-before
关系,有以下规定:
happens-before
于该线程中的任意后续操作happens-before
于随后对这个锁的加锁volatile
变量规则:对一个 volatile
域的写, happens-before
与任意后续对这个 volatile
域的读happens-before
B , 且 B happens-before
C ,那么 A happens-before
Cstart()
规则:如果线程A执行操作ThreadB.start()
(启动线程B),那么A线程的ThreadB.start()
操作happens-before
于线程B中的任意操作。join()
规则:如果线程A执行操作ThreadB.join()
并成功返回,那么线程B中的任意操作happens-before
于线程A从ThreadB.join()操作成功返回。如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e
,他会拉你们进群。