在前面的文章中,我们深入了解了 Java 内存模型,知道了 Java 内存模型诞生的意义,以及其要解决的问题。最终我们知道:Java 内存模型就是定义了 8 个基本操作以及 8 个规则,只要遵守这些规则的并发操作,那么它们就是安全的。
即使强如树哥的人,看了这 16 条规则也很头疼。它们太过于繁琐了,非常不利于我们日常代码的编写。为了能帮助编程人员理解,于是就有了与其相等价的判断原则 —— 先行发生原则,它可以用于判断一个访问在并发环境下是否安全。
到这里,我们需要明白:happens-before 原则是对 Java 内存模型的简化,让我们更好地写出并发代码。就像 Java 语言等高级语言,之于汇编语言、机器语言等低级语言一样,可以让编程人员免受「皮肉之苦」。
happens-before 指的是 Java 内存模型中两项操作的顺序关系。例如说操作 A 先于操作 B,也就是说操作 A 发生在操作 B 之前,操作 A 产生的影响能够被操作 B 观察到。这里的「影响」包括:内存中共享变量的值、发送了消息、调用了方法等。
举个很简单的例子:下面代码里 i=1
在线程 A 中执行,而 j=i
在线程 B 中执行。因为 i=1
操作先于 j=i
执行,那么 i=1
操作的结果就应该能够被线程 B 观察到。
// 在线程 A 中执行
i = 1;
// 在线程 B 中执行
j = i;
Java 内存模型下一共有 8 条 happens-before 规则,如果线程间的操作无法从如下几个规则推导出来,那么它们的操作就没有顺序性保障,虚拟机或者操作系统就能随意地进行重排序,从而可能会发生并发安全问题。
Java 语言无须任何同步手段保障,就能成立的先行发生规则,就只有上面这些了。下面举个例子来说明如何用这些规则去判断操作是否具备顺序性,是否是线程安全的。
private int value = 0;
public void setValue(int value){
this.value = value;
}
public int getValue(){
return value;
}
上面的代码是一组很普通的 getter/setter 方法。假设线程 A 和 B,线程 A 先(时间上的先后)调用了 setValue(1)
,之后线程 B 调用了同一个对象的 getValue()
,那么线程 B 收到的返回值是什么?
我们依次分析一下先行发生原则中的各项规则:
因此,即使我们知道线程 A 在操作时间上先于线程 B,但我们还是无法确定线程 B getValue()
方法的返回结果。换句话说,这里面的操作不是线程安全的。
那怎么修复这个问题呢?
我们至少有两种比较简单的方案可以选择:
getter/setter
方法都定义为 synchronized
方法,这样就可以套用管程锁定规则。value
定义为 volatile
变量,由于 setter
方法对 value
的修改不依赖 value
的原值,满足 volatile
关键字使用场景,这样就可以套用 volatile
变量规则来实现先行发生关系。通过上面这个案例,我们知道:一个操作时间上线发生,不代表这个操作会「先行发生」。 那如果一个操作「先行发生」,是否就能推导出这个操作必定是时间上先发生呢?其实并不能,因为有可能发生指令重排序。
// 如下操作在同一个线程中执行
int j = 1;
int j = 2;
上述代码在同一线程中执行,根据程序执行次序规则,int i = 1;
的操作先行发生于 int j = 2;
,但 int j =2
的代码有可能被处理器先执行,因为它们不相互依赖,不影响先行发生原则的正确性。
上述这两个案例综合起来证明了一个结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。
happens-before 原则一共有 8 条原则,它是对 Java 内存模型规则的简化,帮助编程人员提高编程效率。
时间先后顺序与先行发生原则之间基本没有太大的关系,我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。