happen-before
happen-before
前言
学习happens-before的目的不是只限于知道这些规则的存在,而是要进一步知道如何实现和维护这些happens-before关系,在代码中加以注意。
Happens-before 规则是从java代码设计层面保证有序性和可见性的机制。本文将会以图示、样例代码和解释相结合的方式,力图阐述清楚happens-before的原理,为理解如何保证线程安全性打下扎实的基础。
关于happens-before关系,java语言说明中有如下的描述:
Two 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. --Java Language Specification Ch 17.4.5.
Happens-before Order
Java代码编译成计算机指令后,计算机在执行指令时会进行一定程度的重排序,hb规则相当于Java设计者和开发者之间的约定,屏蔽了计算机的层的细节,从java层面和开发者建立约定,约定的内容是如下这样:
java语言的设计者建立了一套happens-before规则,如果开发者在相应的场景下确保两个操作之间遵从了happens-before规则,那java会为开发者保证,遵从了happens-before规则的多线程操作具有实际的happens-before关系,进而可以保证共享变量的可见性。
这些happenns-before关系大多需要开发者自己保证,而于此同时,java内置就已经实现了一系列happens-before关系,这些关happens-before 关系天然存在,而不需要开发者自行维护。
警惕误区
网上绝大多数的文章在描述happens-before关系时,关于已经存在还是需要开发者留心维护这个点上上没有说清楚。
官方文档所描述的happens-before并非陈述句,不是在表述一个客观事实,而是一个应该补上一个should,完整的语义应该为: Action A should have a happens-before relationship with action B, in order to ensure result of action A is visible to action B.
如果我们把需要我们花气力维护的happens-before关系当成了天然存在的关系,例如,这样的一条规则:“* A write to a volatile field happens-before every subsequent read of that field.”,这是一条需要开发者维护的规则,如果开发者直接把这条规则当成了固有事实,无论怎样的代码顺序都可以保证“对volatile变量的写一定发生在读之前”,那么线程安全就完全得不到保障了。
因此,后文对于happens-before介绍将主要分为两类:即已经存在和开发者自行保证。
Happens-before规则
Happens-before关系可传递性
在学习下面的happens-before规则之前,我们需要了解到的一个已有规则是,happens-before关系具有可传递性。用hb(a,b)表示a happens-before b,而如果 hb(b,c),则我们可以得知hb(a,c)。可传递性这一固有特性将帮助我们顺利理解后面的诸多规则。
已经存在(需要了解)
这一部分happens-before规则已经由java语言设计者实现,故对于开发者而言它们是固有存在的事实,了解他们有助于我们清楚代码为什么可以保证可见性,同时在复杂的情况有分析可见性问题的能力。
1. 单线程规则
我们,都知道,在单线程内部,对于开发者而言,可以确保写在前面的操作会happens-before之后的操作。尽管计算机底层的指令和java代码的顺序会有比较大的差异,但其中的细节jdk会去处理,java可以向开发者保证,单线程所有前面代码的操作结果对于后面代码是可见的。对于这一条规则无需赘述,绝大多数开发者在编码的第一天都是默认了这个事实。而这一规则也是其他所有happens-before规则成立的基石。
2. Thread start规则
线程启动规则指出,线程A中启动线程B,那么线程A中在调用ThreadB.start()方法happens-before线程B 中的所有操作。由于可传递性的存在,我们自然也可以得知,A线程中调用ThreadB.start()之前的所有操作均happens-before B线程的所有操作,即A线程在调用ThreadB.start()之前的操作结果对B线程可见。
3. Thread join规则
该规则指出,线程B中的所有操作happens-before线程B的join()方法。同理,根据可传递性,我们也可以确定,线程B中的所有操作如statement1 happens-before 线程A的statement2。
开发者自行保证(需要了解并清楚如何实现)
这一部分的规则需要开发者额外关注,因为java语言没有内置机制保证这些happens-before规则,要实现这些场景下的可见性,需要开发者自行保证这一关系的存在。如果缺少这些关系,则java无法保证一个线程的操作结果对另一个线程的可见性。
4. volatile变量规则
如果希望A线程的对volatile变量的修改对B线程可见,那么A线程对volatile的变量的写应该happens-before B线程对这个volatile变量的读。
即如果保证线程A对一个volatile变量的写发生在另一个线程B对于这个变量的读之前,在A对这个变量的操作对B可见。
下面通过一个简单的代码样例验证这一规则。
public class TestVolatileHappensBefore {
static volatile int shared = 0;
static class WriteTask implements Runnable {
@Override
public void run() {
shared = 5; //1. write to the volatile shared variable
}
}
static class ReadTaslk implements Runnable {
@Override
public void run() {
System.out.println(shared);//2. read to the volatile shared variable
}
}
public static void main (String[] args) {
Thread tWrite = new Thread(new WriteTask());
Thread tRead = new Thread(new ReadTaslk());
tWrite.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
//just wait to ensure order
}
tRead.start();
}
}
这段代码的运行结果显而易见,会打印shared的值为5。因为遵从了volatile变量的happens-before规则,故注释中1的操作结果对2可见。如果仅展示正例则无法证明这一规则存在的必要性。而下面的这个反例则可以证明我们必须确保这一 happens-before 关系。
public static void main (String[] args) {
Thread tWrite = new Thread(new WriteTask());
Thread tRead = new Thread(new ReadTaslk());
tRead.start();
tWrite.start();
}
反面例子中仅更改了对于volatile变量读写线程的开始顺序,即我们没有严格遵守volatile变量的happens-before规则,那这样的代码运行结果会如何呢?
在短短数次的重复运行中,shared变量的值有时为初始值0,有时为被写进程修改后的值5。也就是更新后的shared变量的值无法保证其对其他线程的可见性。故如果需要保证volatile变量的可见性,我们需要满足volatile变量的happens-before规则。
5. 锁操作规则(synchronized及实现了Lock接口实现)
如果A线程是解锁操作,之后另一个B线程执行加锁操作,如果希望A的操作结果对B可见,那么 A应该happens-before B
同样通过一段样例代码展示该规则的正面案例,依然是线程先读后写的情况,同步机制依赖显示锁ReentrantLock:
public class TestLockHappensBefore {
static int a = 0;
static ReentrantLock lock = new ReentrantLock();
public static void modify1() {
lock.lock();
try {
System.out.println("a= " + a + " with " + Thread.currentThread().getName());
} finally {
a = 10;
lock.unlock();
}
}
public static void modify2() {
lock.lock();
try {
System.out.println("a= " + a + " with " + Thread.currentThread().getName());
} finally {
a = 5;
lock.unlock();
}
}
public static void main(String[] args) {
Thread thread0 = new Thread() {
@Override
public void run() {
modify1();
}
};
Thread thread1 = new Thread() {
@Override
public void run() {
modify2();
}
};
thread0.start();
thread1.start();
}
}
thread0打印当前a的值并将a赋值为10,thread1同样打印当前a的值并将其赋值为5。
而如果将启动顺序倒置
thread1.start();
thread0.start();
在这个样例代码中不存在关系的违反,但满足不同的happens-before关系,根据规则,则会有不同的结果,这需要开发者在开发过程中留心观察,理清逻辑。
6.各式各样的组合场景下的happens-before规则
这就是一个无穷无尽的话题了,在组合的场景下,只有符合了happens-before的线程两两之间能够保证可见性,而不一定可以保证所有线程的互相可见性,要处理好这种情况则需要开发者对
各种场景的happens-before规则烂熟于胸,利用好规则的可传递性,梳理清楚线程交互逻辑,才有可能处理好所有线程间的可见性问题。