从 JDK 5开始,Java 使用新的 JSR-133 内存模型,使用 happens-before 的概念来阐述操作间的可见性。
JSR-133 对Happens-Before 的定义:
Happens-Before RelationshipTwo actions can be ordered by a happens-before relationship. If one action > happens-before another, then the first is visible to and ordered before the second. It should be stressed that a happens-before relationship between two actions does not imply that those actions must occur in that order in a Java platform implementation. The happens-before relation mostly stresses orderings between two actions that conflict with each other, and defines when data races take place. There are a number of ways to induce a happens-before ordering, including:
定义:如果一个操作happens-before另一个操作,那么意味着第一个操作的结果对第二个操作可见,而且第一个操作的执行顺序将排在第二个操作的前面。 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的结果,与按照happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。具体规则如下:
注:说明一下,网上搜出来有的是8条规则,我不知道还有两条哪儿来的,JSR-133 里面只有这六条。网上的还有下面两条:
程序顺序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对 单线程 有效,在多线程环境下无法保证正确性。
监视器锁规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。
volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。
线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。
线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。
传递性规则:提现了happens-before原则具有传递性。
特别强调happens-hefore不能理解为“时间上的先后顺序”。 我们来看如下代码:
public class VolatileTest { private int a = 0; private int getA() { return a; } private void setA(int a) { this.a = a; } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 100; i++) { VolatileTest volatileTest = new VolatileTest(); Thread thread1 = new Thread(() -> { volatileTest.setA(10); }); thread1.start(); Thread thread2 = new Thread(() -> { System.out.print(volatileTest.getA()+" "); }); thread2.start(); } } } 复制代码
上面代码就是一组简单的setter/getter方法,现在假设现在有两个线程 thread1 和 thread2,线程 thread1 先(这里指时间上的先执行)执行setA(10),然后线程 thread2 访问同一个对象的getA()方法,那么此时线程B收到的返回值是对少呢?
0 0 0 0 10 0 10 10 10 0 10 0 10 10 10 10 10 0 10 10 0 0 0 10 0 10 10 10 0 10 0 10 10 10 0 10 10 0 10 10 10 0 0 10 10 0 10 0 10 10 10 10 10 10 10 10 10 10 0 0 0 10 10 0 10 0 10 0 0 0 10 10 0 10 10 10 10 10 10 10 10 10 10 10 0 10 10 10 0 10 10 10 10 10 0 10 0 10 0 0 复制代码
虽然线程 thread1 在时间上先于线程 thread2 执行,但是由于代码完全不适用happens-before规则,因此我们无法确定先 thread2 收到的值时多少。也就是说上面代码是线程不安全的。
从图可以看出:
一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法.
时间先后顺序与happens-before原则之间基本没有太大的关系,所以我们在衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以happens-before原则为准。
简单的说,happens-before 规则就是为了让程序猿更好的理解 JMM 提供的内存可见性而编写的规则,让程序猿能避免去学习编译器和底层编译原理的重排序规则。