先来个示例:

【示例一】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}

public void reader() {
if (v == true) {
//x的值是多少呢?
}
}
}

这里,假设线程 A 执行 writer() 方法,按照 volatile 会将 v = true 写入内存;线程 B 执行 reader() 方法,按照 volatile,线程 B 会从内存中读取变量 v,如果线程 B 读取到的变量 v 为 true,那么,此时的变量 x 的值是多少呢??

这个示例程序给人的直觉就是x的值为 42,其实,x 的值具体是多少和 JDK 的版本有关,如果使用的 JDK 版本低于 1.5,则 x 的值可能为 42,也可能为 0。如果使用 1.5 及 1.5 以上版本的 JDK,则 x 的值就是 42。

看到这个,就会有人提出问题了?这是为什么呢?其实,答案就是在 JDK1.5 版本中的 Java 内存模型中引入了 Happens-Before 规则。

接下来,我们就结合案例程序来说明 Java 内存模型中的 Happens-Before 规则。

1.程序次序规则

在一个线程中,按照代码的顺序,前面的操作 Happens-Before 于后面的任意操作。

例如【示例一】中的程序 x = 42 会在 v = true 之前执行。这个规则比较符合单线程的思维:在同一个线程中,程序在前面对某个变量的修改一定是对后续操作可见的。

2.volatile变量规则

对一个 volatile 变量的写操作,Happens-Before 于后续对这个变量的读操作。

也就是说,对一个使用了volatile变量的写操作,先行发生于后面对这个变量的读操作。

3.传递规则

如果 A Happens-Before B,并且 B Happens-Before C,则 A Happens-Before C。

【示例一】中根据 Happens-Before 规则我们可以得到:

  • x = 42 Happens-Before v = true,程序次序规则。
  • v = true Happens-Before v == true,volatile变量规则。
  • x = 42 Happens-Before v == true,传递规则。

也就是说,如果线程 B 读取到了 v = true,那么,线程 A 设置的 x = 42 对线程 B 就是可见的。换句话说,就是此时的线程 B 能够访问到 x = 42

4.锁定规则

对一个锁的解锁操作 Happens-Before 于后续对这个锁的加锁操作。

例如,下面的代码,在进入 synchronized 代码块之前,会自动加锁,在代码块执行完毕后,会自动释放锁。

【示例二】

1
2
3
4
5
6
7
8
9
10
public class Test{
private int x = 0;
public void initX{
synchronized(this){ //自动加锁
if(this.x < 1){
this.x = 1;
}
} //自动释放锁
}
}

我们可以这样理解这段程序:假设变量 x 的值为 0,线程A执行完 synchronized 代码块之后将 x 变量的值修改为 1,并释放 synchronized 锁。当线程 B 进入 synchronized 代码块时,能够获取到线程 A 对 x 变量的写操作,也就是说,线程 B 访问到的 x 变量的值为 1。

5.线程启动规则

如果线程 A 调用线程 B 的 start() 方法来启动线程 B,则 start() 操作 Happens-Before 于线程 B 中的任意操作。

我们也可以这样理解线程启动规则:线程 A 启动线程 B 之后,线程 B 能够看到线程 A 在启动线程 B 之前的操作。

【示例三】

1
2
3
4
5
6
//在线程A中初始化线程B
Thread threadB = new Thread(()->{
//此处的变量x的值是多少呢?答案是100
});
x = 100; //线程A在启动线程B之前将共享变量x的值修改为100
threadB.start(); //启动线程B

上述代码是在线程 A 中执行的一个代码片段,根据线程启动规则,线程 A 启动线程 B 之后,线程 B 能够看到线程 A 在启动线程 B 之前的操作,在线程 B 中访问到的 x 变量的值为 100。

6.线程终结规则

线程 A 等待线程 B 完成(在线程 A 中调用线程 B 的 join() 方法实现),当线程 B 完成后(线程 A 调用线程 B 的 join() 方法返回),则线程 A 能够访问到线程 B 对共享变量的操作。

【示例四】

1
2
3
4
5
6
7
Thread threadB = new Thread(()-{
//在线程B中,将共享变量x的值修改为100
x = 100;
});
threadB.start(); //在线程A中启动线程B
threadB.join(); //在线程A中等待线程B执行完成
//此处访问共享变量x的值为100

7.线程中断规则

对线程 interrupt() 方法的调用 Happens-Before 于被中断线程的代码检测到中断事件的发生。

例如,下面的程序代码。在线程 A 中断线程 B 之前,将共享变量 x 的值修改为 100,则当线程 B 检测到中断事件时,访问到的 x 变量的值为 100。

【示例五】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    //在线程A中将x变量的值初始化为0
private int x = 0;

public void execute(){
//在线程A中初始化线程B
Thread threadB = new Thread(()->{
//线程B检测自己是否被中断
if (Thread.currentThread().isInterrupted()){
//如果线程B被中断,则此时X的值为100
System.out.println(x);
}
});
//在线程A中启动线程B
threadB.start();
//在线程A中将共享变量X的值修改为100
x = 100;
//在线程A中中断线程B
threadB.interrupt();
}

8.对象终结规则

一个对象的初始化完成 Happens-Before 于它的 finalize() 方法的开始。

【示例六】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {

public Test(){
System.out.println("构造方法");
}

@Override
protected void finalize() throws Throwable {
System.out.println("对象销毁");
}

public static void main(String[] args){
new Test();
System.gc();
}
}

运行结果如下所示。

1
2
构造方法
对象销毁

评论




博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议

载入天数...载入时分秒... 本站使用 Volantis 作为主题 鲁ICP备-20012065号