前言
看完《Java并发编程的艺术》整本书之后,再次回顾并发编程中的Java内存模型,有了一写自己见解,这里接着前两次的文章继续做一个总结。
final域的内存语义
关于Java的final修饰符的一些基础知识可以参考我的这篇文章Java final关键字小结。今天要介绍的就是final在并发编程中,Java内存模型如何保证final的线程同步。
1. final域的重排序规则
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
第一句话是保证了通过类的实例获取到的final域的值,一定会是写入后的有效值。不过由于final域只会被写入一次,所以不需要担心后续再会有对final域的写操作。
第二句话中的两个操作存在依赖关系,符合as-if-serial语义,所以这两个操作不会被重排序。
public class FinalExample {
int i; //普通变量
final int j; //final变量
static FinalExample obj;
public FinalExample() { //构造函数
i = 1; //写普通域
j = 2; //写final域
}
public static void writer() { //写线程A执行
obj = new FinalExample();
}
public static void reader() { //读线程B执行
FinalExample object = obj; //读对象引用
int a = object.i; //读普通域
int b = object.j; //读final域
}
这段代码是《Java并发编程的艺术》第三章的原代码,由于final对重排序的限制,就保证了b读取的值一定会是2;但是a读取到的值有可能为1也有可能为0。
2. final引用“溢出”
如果在构造函数内部,被构造对象的引用为其他线程可见,那么对象引用就会存在“逃逸”现象。此时final域就不能保证线程同步了。
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample() {
i = 1; //1写final域
obj = this; //2 this引用在此“逸出”
}
public static void writer() {
new FinalReferenceEscapeExample();
}
public static void reader() {
if (obj != null) { //3
int temp = obj.i; //4
}
}
上面代码的操作1和操作2存在重排序,obj先获得对象引用,再对i进行初始化。那么对于操作3条件为真时i未被初始化,然后操作4读取到i未被初始化时的值。
happens-before
对于编译器和处理器来说,它们希望内存模型的约束越少越好,这样就能做更多的性能优化操作。最常见的就是重排序。而程序员则希望程序能得到一个预期的结果,那么就需要一个强内存模型来支持。
1. happens-before的概念
这个是我个人理解:如果操作B的执行依赖与A操作的结果,那么就说A happens-beforeB。
最常见的例子就是:int a = 1;//1 int b = a + 2; //2这两个操作必须保证1先于2执行,不然程序会得到异常结果。
一般来说,我们认为存在依赖关系的操作都应该有happens-before关系,但是在多线程编程里,本来应该具有happens-before关系的操作往往都不存在了。所以我们需要一些技术告知Java内存模型去对一些操作加上happens-before操作。比如前面提到的final、volatile、锁等技术。
事实上,Java内存模型只会禁止会改变程序执行结果的重排序,而对于那些不会改变程序执行结果的重排序,JMM不做禁止。
JMM其实是在遵循一个基本原则:只要不改变程序的执行结果,编译器和处理器怎么优化都行。
2. happens-before定义和规则
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。
上面两句话有一个让人疑惑的地方就是,第一句话是如果Ahappens-beforeB,那么A就必须先于B执行,而第二句话的意思就是A可能在Java平台上不先于B执行。我目前的理解是这样的。
int a;
a = 0;//A
int b = a + 1;//B
上面两个操作A happens-beforeB,由于B和A重排序不影响程序的最终执行结果,所以Java内存模型允许A和B的重排序。因为a在未写入值之前默认值为0,写入的值也为0,所以B操作得到的结果都为1。
规则:
- 程序顺序规则
- 监视器锁规则
- volatile变量规则
- 传递性
- start规则
- join规则