众所周知,final 关键字在Java语法中用来修饰常亮,不允许修改的数据。那么对于前面提到的volatile 类型的数据相比,除了不能被修改好像对final的写和读和普通的变量并没有什么不同,那么笔者将在这里简单概述下final的内存语义以及其实现的意义。
1、重排序规则
在构造函数内对一个final域的写入,与随后吧这个被构造的对象赋值给一个引用变量,这两个操作之后不能重排序
初次读一个包含final域的对象,与随后初次读这个这个对象的final域,两者之间不能重排序(针对特殊处理器)
首先看下面一段代码
static class FinalWriteExample {int a;final int b;static FinalWriteExample example;public FinalWriteExample(int a, int b) {this.b = b; //1. 对final域写入this.a = a; //2. 对普通域写入}public static void write() {example = new FinalWriteExample(1, 2); // 3.将构造的对象赋值给一个引用变量}public static void read() {FinalWriteExample exampleObj = example;int exampleA = exampleObj.a;int exampleB = exampleObj.b;}}
上面的代码示例中,就是一个非常简单的程序,根据第一条重排序规则要求:构造函数中,第一次对final域的写入(操作 1)和将构造函数赋值给一个引用变量(操作 3)之间不能重排序。
试想一下:倘若没有这个要求,操作1->2->3 可能会被重排序为 2->3->1 那么当其他线程尝试对其这个变量的时候,final域可能还没有完成初始化,由于类的加载器有初始化的步骤,那么b的值将会是0,其后执行操作1,此时final的值又被更新为1。可以看到出现了一个非常严重的缺陷:线程看到的final域的值可能会变,这与final的作用相违背。为了修复这个漏洞 JSR-133 增强了final的语义,在第一次对final域赋值,与将构造的对象赋值给其他引用之间增加StoreStore 内存屏障指令,禁止重排序。
上面的final域的类型是基本类型,倘若是引用类型的话,那么第一条重排序规则可以这样汇总: 构造函数中,第一次对final的写入、对final域的成员变量写入以及将构造的对象赋值给引用变量,三者之间不能出现重排序。
同样的,对于final域的读操作也有重排序要求: 在一个线程中,初次读对象引用与初次读这个对象引用的final域,两者之间不能重排序。但是因为这两者之间有依赖关系,大部分处理器不会进行重排序,所以这个规则只针对一些特殊的处理器。
2、引用提前"溢出"
尽管JMM要求了重排序规则,但是一些特殊的代码还是能够将对象引用在构造方法执行完成之前溢出,修改章节1 的代码,如下:
static class FinalWriteExample {int a;final int b;static FinalWriteExample example;public FinalWriteExample(int a, int b) {FinalWriteExample.example = this; // 4.this引用被提前溢出this.b = b; //1. 对final域写入this.a = a; //2. 对普通域写入}public static void write() {example = new FinalWriteExample(1, 2); // 3.将构造的对象赋值给一个引用变量}}
操作4 在构造方法未完成之前就完成了引用赋值,倘若其他线程在读取example变量,那么也会出现fianl变量会变的问题,所以在多线程的编程中,我们要尽量避免在构造方法完成之前引用溢出的问题。
3、总结
综上所述,我们可以知道在针对 final的操作中,我们的目的主要是确保 final域在构造方法赋值之前被成功的正确的初始化。只要对象被正确的构造初始化,那么我们就不必使用同步操作就可以使得其他线程正确的读取到final的值。