2详解Happens-Before原则(解决并发编程可见性、有序

2019-09-21  本文已影响0人  SuperMarry

并发的三个特性:原子性,可见性,有序性

可见性 -> 缓存
有序性 -> 编译优化

volatile 使用

介绍 volatile是在c语言的产物,他的本意是声明一个变量禁止使用cpu缓存。

在 jdk1.5之后,volatile还被赋予了,局部禁止指令优化的功能,也就是对volatile 变量之前的操作对于再次访问volatile 变量时,必须是可见的。
举个例子


class Test {
int x = 0;
volatile boolean flag = false;
//写线程A
public void writer() {
x = 42;
flag = true;
}
//,读线程B,现在线程A完成了对flag的赋值,
public void reader() {
if (flag == true) {
// 这里 x 会是多少呢?
}
}
}

假设现在有两个线程,写线程A,读线程B,现在线程A完成了对flag的赋值,flag = true,
现在读线程B判断flag的值,为true,并且输出 x 的值,那么 x会是多少呢。
在jdk1.5之前, x 的值可能是 0,也有可能是 42,因为对于 x 和 flag的赋值是可以优化的,具有不确定性,但是在jdk1.5之后,对于volatile增加了局部限制优化的功能,在flag之前的操作,也就是对 x 的赋值在再次访问 flag时,必须是完成可见的,所以jdk1.5之后,可以明确知道输出的值是42。

Happens-Before原则
对于Happens-BeforeHappens-Before原则,我们无需去尝试去翻译成中文理解其含义,我们要知道的是:Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则.
Happens-Before原则总共有八条,这是并发编程中最为重要的原则,接下来一一分析。

原则一:程序次序规则
在一个线程之中,按照代码顺序,前面的操作对于后续操作是可见的。
回到我们上边的例子,其实这个原则的最正确的说法应该是:在一个线程之中,按照代码顺序,看起来前面的操作对于后续操作是可见的。虽然最终执行的结果都一样,但是jvm还是会对代码进行指令重排,如 “x = 42; flag = true”,jvm可能先对 x 进行赋值,也可能对 flag进行赋值,但是对哪个变量进行操作对整个程序来说并没有什么区别。

public void writer() {
x = 42;
flag = true;
}

假设我们这段代码变为

public void writer() {
a=13  //1
x = 42;  //2
flag = true; //3
a=x*a; //4
}

jvm可能会对这段代码进行指令重排但是不管怎么重新排序,一定会在操作4之前,完成操作1,2,因为操作4对操作1和操作2有数据依赖。但是即使是优化,整段代码下来就跟程序顺序执行一样。
在分析原则一时,我特意表明是在 一个线程中 ,那如果不在一个线程中会怎么样呢?
class Test {
int x = 0;
boolean flag = false;
//写线程A
public void writer() {
x = 42;
flag = true;
}
//,读线程B,现在线程A完成了对flag的赋值,
public void reader() {
if (flag == true) {
// 这里 输出的 x 会是多少呢?
}
}
}
在这里x输出的值可能是 0,也可能是42 。

原则二:volatile变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作
(在这里我们看不出原则二的作用是什么,我们结合原则三一起分析)

原则三:传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C.
我们回到文章开始的例子,这里将代码再贴一遍。

class Test {
int x = 0;
volatile boolean flag = false;
//写线程A
public void writer() {
x = 42;
flag = true;
}
//,读线程B,现在线程A完成了对flag的赋值,
public void reader() {
if (flag == true) {
// 这里 x 会是多少呢?
}
}
}

问:,写线程A,读线程B,现在线程A完成了对flag的赋值,flag = true,
现在读线程B判断flag的值,为true,并且输出 x 的值,那么 x会是多少呢?
在线程A中,通过volatile可以确保 x = 42 先行发生于 flag=true。
在线程B中,通过原则二可以确保 flag==true 先行flag==true先行发生于 flag=true
通过原则三就可以得出这样的顺序 x = 42 flag=true flag==true 所以最终输出的x值为
42
注:volatile可以确保 x = 42 先行发生于 flag=true是在jdk1.5之后的功能

原则四:线程启动规则
它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

Thread B = new Thread(()->{
// 主线程调用 B.start() 之前
// 所有对共享变量的修改,此处皆可见
// 此例中,a=77
});
// 此处对共享变量 a 修改
a= 77;
// 主线程启动子线程
B.start();

原则五:线程 join() 规则
主线程 A 通过调用子线程 B 的 join() 方法实现,当子线程 B 完成后,主线程能够看到子线程对共享变量的的操作。

原则六:线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

原则七:锁定规则
一个unLock操作先行发生于后面对同一个锁的lock操作


synchronized (this) { 
// x 是共享变量
if (this.x < 12) {
this.x = 12;
}
} 

线程A占有此锁,线程B访问此段程序,B在要是能成功访问(获得锁),线程A必然已经释放锁。

原则八:对象finalize规则
一个对象的初始化完成先行发生于他的finalize()方法的开始。

上一篇下一篇

猜你喜欢

热点阅读