From:Java并发编程的艺术
- 目录
BiBi - 并发编程 -0- 开篇
BiBi - 并发编程 -1- 挑战
BiBi - 并发编程 -2- volatile
BiBi - 并发编程 -3- 锁
BiBi - 并发编程 -4- 原子操作
BiBi - 并发编程 -5- Java内存模型
BiBi - 并发编程 -6- final关键字
BiBi - 并发编程 -7- DCL
BiBi - 并发编程 -8- 线程
BiBi - 并发编程 -9- ReentrantLock
BiBi - 并发编程 -10- 队列同步器
BiBi - 并发编程 -11- 并发容器
BiBi - 并发编程 -12- Fork/Join框架
BiBi - 并发编程 -13- 并发工具类
BiBi - 并发编程 -14- 线程池
BiBi - 并发编程 -15- Executor框架
final在并发中的应用
当对象被正确构造时,即被构造对象的引用在构造函数中没有【逸出】,那么不需要使用同步就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值。
在旧的Java内存模型中,线程可能看到final域的值是会改变的。因为初始值有默认值0、false、null。通过为final域增加读/写重排序规则,来保证初始化的安全。
final域的重排序规则
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
写final域的重排序规则可以保证:在对象引用为任意线程可见之前,对象的final域已经被正确初始化了,而普通域不具有这个保障。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。编译器会在读final域操作的前面插入一个LoadLoad屏障。读对象的普通域的操作可能被处理器重排序到读对象应用之前而导致错误,读对象的final域则不会。即在读一个对象的final域之前,一定会先读取包含这个final域的对象的引用。而普通域,则不能保证。
final域为引用类型
public class FinalExample {
final int[ ] intArray;
static FinalExample obj;
public FinalExample () {
intArray = new int[1]; // 1
intArray[0] = 1; // 2
}
public static void writeOne() {
obj = new FinalExample(); // 3
}
}
写final域引用的重排序规则:在构造函数内对一个final引用的对象的成员域的写入【intArray[0] = 1】,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。即操作 1 和操作 3 不能重排序,并且操作 2 和操作 3 也不能重排序。
对象【逸出】问题
上述讲到的,写final域的重排序规则可以保证:在对象引用为任意线程可见之前,对象的final域已经被正确初始化了。其实这个还需要一个前提:在构造函数内部,不能让这个被构造对象的引用为其它线程所见,也就是对象引用不能在构造函数中【逸出】。
public class FinalExampleT {
final int i;
static FinalExampleT obj;
public FinalExampleT () {
i = 1; // 1
obj = this; // 2 "逸出"
}
}
因为操作 1 和操作 2 可能会重排序,导致能够获取到对象FinalExampleT的引用,但是FinalExampleT中的final域i,还没有初始化,这不符合jvm的设计规范。
网友评论