JMM之重排序

重排序定义

在前面我们提到过,重排序是编译器和处理器为了优化程序性能而对指令序列重新排序的一种手段。但是我们也知道代码不可能毫无原则的进行重排序,如果是毫无原则的进行重排序,那么我们的代码将无法获得预期的结果。因此重排序必须满足如下原则:

  1. 在单线程中不改变运行结果
  2. 操作不具备数据依赖性

那这两条原则如何理解么,我们先来看看下面的定义

数据依赖性

数据依赖性的意思是,若果两个操作访问同一个变量,并且其中一个操作是写操作,那么这两个操作就具备数据依赖性。

as-if-serial语义

as-if-serial语义的意思是,不管如何重排序,在单线程中,程序的执行结果不能被改变。编译器、处理器和runtime都不行遵守as-if-serial语义。注意:as-if-serial只对单线程有效,对多线程无效。

了解了数据依赖性和as-if-serial后,我们看看如下示例:

int a = 1;      // A
int b = 2;      // B 
int c = a * b;  // C

在上述示例中,我们根据数据依赖性可以分析出,A和C具备数据依赖性,B和C具备依数据赖性,A和B之前不存在数据依赖性关系,因此编译器和处理器在进行重排序时,A和B可以进行重排序,但是A和C,以及B和C是不能进行重排序的。因为尽管A和B进行了重排序,但是他们不影响程序在单线程中运行的结果。

在这里,我们联想前面讲到过的happens-before,可以看出上面代码存在3个happens-before关系。它们分别是:

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

在这里LZ再次强调,happens-before与时间上的先后完全没有关系,happens-before仅仅要求,前一个操作的结果对后一个操作可见。在这里操作A的结果不需要对B可见,重排序操作A和操作B的结果与按照A happens-before B 的执行结果一致。在这种情况下JMM认为这种重排序并不非法,JMM允许这种重排序。

重排序对多线程的影响

在了解了重排序的概率和数据依赖性以及as-if-serial的定以后,我们知道重排序在单线程中是没有影响的,那么重排序对多线程是否有影响呢?我么首先来看看下面的代码示例:

class ReorderExample {
	int a = 0;
	boolean flag = false;

    public void writer() {
		a = 1; // 1
		flag = true; // 2
	} 
    public void reader() {
		if (flag) { // 3
			int i = a * a; // 4
		}
	}
}

假设现在有A和B两个下次,此时A线程执行write()方法,B线程执行reader()方法,那么在线程B在执行操作4时,能否获取到a=1呢?答案是不一定。

由于操作1 和操作2没有数据依赖性,因此编译器和处理器能够对这2个操作进行重排序,当操作1和操作2进行重排序时,此时程序的执行时序图是这样子的

重排序分析一

如上图所示,操作1和操作2进行了重排序。程序在执行时,线程A首先写标记变量flag,随后线程B读取这变量,由于条件判断为真,线程B将读取变量a,此时变量a还没有被先A写入,在这里多线程程序的语义被重排序破坏掉了。

同样,操作3和操作4也可以进行重排序,但是操作3和操作4存在控制依赖的关系,即当操作3为真时,操作4才可以执行。当代码中存在控制依赖关系时,会影响指令序列执行的并行度。为此,编译器和处理器采用猜测执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前计算a*a,然后把计算的结果保存到重排序缓存中,当操作3的结果为真时,就把改计算结果写入变量i中。

通过上面的分析,我可以得出结论:重排序对单线程的执行结果没有影响,但是在多线程中,重排序可能会改变程序执行的结果

扫码关注公众号,回复1024 获取最新大厂面试资料

JMMJava Memory Model(Java内存模型)的缩写,它定义了Java程序中多线程并发访问共享变量时的行为规范。JMM的设计目的是为了保证多线程程序在不同的硬件和操作系统平台上的可移植性,并提供一定的可见性和原子性保证。 重排序是指编译器和处理器对指令的执行顺序进行优化的一种技术。由于指令的重排序,原本在代码中的先后顺序可能被打乱,但是这种重排序不会影响单线程程序的执行结果。然而对于多线程程序来说,重排序可能会导致线程安全问题。 可见性是指当一个线程对共享变量进行了修改后,其他线程是否能够立即看到这个修改。在JMM中,由于为了性能考虑,对于普通的共享变量,JVM会对读写操作进行重排序,从而可能导致一个线程修改了共享变量后,其他线程无法立即看到这个修改的结果。为了保证可见性,我们可以使用volatile关键字来修饰共享变量,强制线程对共享变量的读写操作都通过主内存进行,从而解决了可见性问题。 原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行。在多线程程序中,对于一些涉及到多步操作的操作,如果没有保证原子性,可能会导致线程安全问题。为了保证原子性,可以使用synchronized关键字或者Lock来进行同步,保证任意时刻只有一个线程可以执行这些操作,从而解决了原子性问题。 总之,JMM重排序、可见性和原子性是为了解决多线程程序中的线程安全问题而提出的。重排序可能导致程序执行结果和预期不符,可见性问题可能导致线程无法看到其他线程对共享变量的修改,原子性问题可能导致操作只完成了一部分而不是全部。通过合理地使用volatile关键字和同步机制,可以有效地解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值