说到final你确定知道它是Java中的关键字,那么它所在Java中的做用你知道吗?不知道的话,请前往这篇了解下http://www.javashuo.com/article/p-vewbmfte-eq.htmlhtml
今天咱们来讲说final域在JMM中的内存语义。函数
开门见山,对于final域,编译器和处理器必定要遵照两个重排序规则(JSR-133才加强了final域):this
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这个两个操做不能被重排序。
spa
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操做之间不能重排序。线程
下面咱们经过案例来讲明这两点(假设线程1执行writer(),随后另外一个线程执行reader()方法):code
public class FinalExample { static volatile boolean flag = true; int i = 0; final int j; static FinalExample obj; public FinalExample() { // 构造函数 i = 1; // 写普通域 j = 2; // 写final域 } public static void writer() { // 线程1写入 obj = new FinalExample(); } public static void reader() { // 线程2读取 FinalExample example = obj; // 读对象引用 System.out.println(example.i); // 读普通域 System.out.println(example.j); // 读final域 } }
写final域的重排序规则禁止把final域的写重排序到构造函数以外。这个规则的实现包含下面两个方面:htm
1)JMM禁止编译器吧final域的写重排序到构造函数以外。对象
2)编译器会在final域的写以后,构造函数return以前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数以外。blog
因此线程1执行顺序以下图(其中写普通域的顺序没法保证,理论上是存在下面三种状况的,要想验证普通域是否有重排序的结果有点难,由于没法保证线程1把普通域重排序后,线程2可以读取它以前的0值):排序
读final域的重排序规则是:在一个线程中,初次读这个对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操做。其中编译器会在读final域操做的前面插入一个LoadLoad屏障。因为插入了loadLoad屏障,读普通域i的操做是不会重排序到读final域,可是不保证它会重排到读对象引用这个操做的前面。因此线程2的一个执行顺序就能想象到了,这里就不画线程2的执行顺序图了。
若是finaly域为引用类型,JMM中是怎么处理的呢?对于引用类型,写final域的重排序规则对编译器和处理器增长了以下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。
以上及其以上都要注意:只针对于构造函数方法内。另外要想以上规则确保,还须要一个条件:在构造函数内部,不能让这个被构造对象的引用被其余线程可见,也就是对应引用不能再构造函数中“逸出”。以下案例:
class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample() { i = 1; // 1 obj = this; // 2 this引用逸出 } public static void writer() { // 线程1 new FinalReferenceEscapeExample(); } public static void reader() { // 线程2 if (obj != null) { System.out.println(obj.i); } } }
上面程序,第一步写final域与第二步是不保证重排序的。因此当第一步与第二步重排以后,线程1执行完这步(obj = this)后,时间片分给第二个线程执行,那么线程2将会获取final域初始化以前的值,这确定就违背了程序的初衷。