volatile 和 final
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这个两个操作就存在数据依赖性。数据依赖性分下列三种类型:
名称 | 代码实例 | 说明 |
---|---|---|
写后读 | a = 1; b = a; | 写一个变量后,再读这个变量 |
写后写 | a = 1; a = 2; | 写一个变量后,再写这个变量 |
读后写 | a = b; b = 1; | 读一个变量后,再写这个变量 |
上面3中情况,只要重排序两个操作的执行顺序,程序的运行结果就会被改变。所以编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。
一个生动的例子:
double pi = 3.14 //A
double r = 1.0 // B
double area = pi * r * r; //C
A 和 C存在数据依赖关系,同时B 和 C之间也存在数据依赖关系。因此C不会被重排序到A 和 B的前面。
A 和 B之间没有数据依赖关系,编译器和处理器可以重排序A 和 B之间的执行顺序。
重排序对多线程的影响
一个生动的例子
public class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a * a; // 4
......
}
}
}
假设有两个线程A和B,A首先执行writer() 方法,随后B线程接着执行reader() 方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入呢?答案是:不一定能看到。
由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。
如果操作1和操作2重排序时,如下图:
如果操作3和操作4重排序时,如下图:
happens - before 规则
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系执行的结果一致,那么这种重排序并不非法。
- 程序顺序规则:一个线程中的每个操作,happens-before与该线程中的任意后续操作
- 锁规则:对一个锁的解锁,happens-before与随后对这个锁的加锁
- volatile规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
- start()规则:如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start()操作happens-before与线程B中的任意操作
- join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before与线程A从ThreaB.join()操作成功返回。
- 传递性:如果A happens-before B,B happens-befo C,那么A happens-before C。
volatile内存语义
一个生动的例子
class VolatileExample{
int a = 0;
volatile flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a * a; // 4
......
}
}
}
假设线程A先执行writer()方法,线程B执行reader()方法。根据happens-before规则,这个过程建立的happens-before关系分类3类:
- 根据程序次序规则,1 happens-before 2;3 happens-before 4。
- 根据volatile规则,2 happens-before 3。
- 根据传递性规则,1 happens-before 4。
程序时序图如下图所示:
volatile保证:
- volatile写之前的操作不会被重排序到volatile之后。
- volatile读之后的操作不会被重排序到volatile之前。
- 第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
final内存语义
一个生动的例子
class FinalExample {
int i; // 普通变量
final int j; // final 变量
static FinalExample obj;
public FinalExample() {
i = 1;
j = 2;
}
public void writer() {
obj = new FinalExample();
}
public void reader() {
FinalExample object = obj;
int a = object.i;
int b = object.j;
}
}
假设线程A执行writer()方法,随后另一个线程B执行reader()方法。
写final域的重排序规则的程序时序图如下:
读final域的重排序规则的程序时序图如下:
final保证:
- 在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了。
- 在读一个对象的final域之前,一定会先读包含这个final域的对象的引用,而引用对象的final域肯定初始化过了。
final引用不能从构造函数溢出
一个生动的例子
class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample() {
i = 1;
obj = this;
}
public void writer() {
new FinalReferenceEscapeExample();
}
public void reader() {
if (obj != null) {
int temp = obj.i;
}
}
}
假设线程A执行writer()方法,随后另一个线程B执行reader()方法。
执行程序时序图可能如下:
在构造函数返回前,被构造对象的引用不能为其他线程所见,因为此时的final域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到final正确初始化之后的值。
参考
- Java并发编程的艺术[书籍]