在原子性、可见性、有序性这篇文章中,我们提到了为了实现并发编程、提高系统效率,我们引入了缓存、线程、编译优化,也因此引出了可见性、原子性、有序性问题。在并发编程中,我们就需要解决这三个问题。
原子性问题的解决放在下一篇。为了解决可见性、编译优化后指令顺序改变,我们需要禁用缓存、禁止编译优化。而这个过程是通过volatile、synchronized、final三个关键字以及6个happens-before规则来实现的。
happens-before的意思是:前面一个操作的结果对后续操作是可见的。
1.单线程规则:在一个线程中,前面的操作happens-before后面的操作。
2.volatile变量规则:一个对volatile变量的写操作,happens-before于后续的读操作
3.传递性规则:如果A happens-before B,B happens-before C。那么,A happens-before C
4.管程中的锁:对一个锁的解锁happens-before后续的加锁
5.线程start()规则:主线程A启动子线程B后,B能够看到A在启动B前的操作
6.线程join()规则:主线程A等待子线程B完成,当B完成后,A能看到B的操作
然后我们通过几个例子来加深理解:
规则1 2 3的应用
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 读取到x = 42
}
}
}
根据规则1:x=42 happens-before v=true
根据规则2:写变量v=true happens-before 读变量v=true
根据规则3:x=42 happens-before 读变量v=true
所以说线程B读到的x=42.
规则4的应用
synchronized (this) { // 此处自动加锁
// x 是共享变量, 初始值 =10
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁
假设由线程A先进入同步代码块,由于规则4,后进入的线程B能够看到A修改后的x值
规则5的应用
Thread B = new Thread(()->{
// 主线程调用 B.start() 之前
// 所有对共享变量的修改,此处皆可见
// 此例中,x==77
});
// 此处对共享变量 var 修改
x = 77;
// 主线程启动子线程
B.start();
线程A修改共享变量x=77后启动子线程B,B可以看到x=77
规则6的应用
Thread B = new Thread(()->{
// 此处对共享变量 x 修改
x = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程 B 可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用 B.join() 之后皆可见
// 此例中,x=66
子线程B修改共享变量x=66,主线程A等待B结束后,可以看到x=66