final 关键字可以声明成员变量、方法、类以及本地变量。
通过为final域增加写和读重排序规则,可以为Java程序员提供初始化安全保证:只要对象是正确构造的,不需要使用同步(指lock和volatile的使用)就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值。
一、final 使用
一旦你将引用声明作final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。
1.1、final 变量
final 可以声明成员变量和本地变量(局部变量)。
1)必须显示初始化
final 修饰变量,必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
在类成员中
final
如果和static
一起使用,作为类常量使用,此时必须在声明时初始化赋值。本地变量(局部变量)也必须在声明时赋值,不属于类变量,无法在构造函数中赋值。
1 | public class FinalTest { |
2)基本数据类型和引用类型变量
final 修饰基本数据类型变量,不可变的是变量值,在初始化后不可改变;
final 修饰引用类型变量,不可变的是引用指向的对象地址,引用指向的对象内容是可以变化的。对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。
1.2、final 方法
final 方法不能被重写。
使用 final 修饰方法的原因:把方法锁定,以防任何继承类修改它的含义,即该方法不能被子类重写。
1.3、final 类
final 类不能被继承。
final 类中的变量可以为final,也可以非final。
final 类中的方法默认为final,因为不会被覆写。
1.4、final其他特点
接口中声明的所有变量本身是final的。
接口中定义的成员变量,默认都是
public static final
的常量(静态字段)。final
与abstract
是相反的,不会一起出现。final
与static
:一起使用,可修饰变量、修饰方法,不可被改变重写,通过类名直接访问。父类中
private final
方法,子类可以重新定义同名方法,这种情况不是重写。
二、final 的重排序规则
对于 final 域,编译器和处理器要遵守两个重排序规则:
在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
原因:编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。
初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。
原因:编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。
1 | public class FinalExample { |
这里假设一个线程 A 执行 writer () 方法,随后另一个线程 B 执行 reader () 方法。下面我们通过这两个线程的交互来说明这两个规则。
2.1、写 final 域的重排序规则
1)分析
writer () 方法只包含一行代码:FinalExample obj = new FinalExample ()
。
这行代码包含两个步骤:构造一个 FinalExample 类型的对象;把这个对象的引用赋值给引用变量 obj。
假设线程 B 读对象引用与读对象的成员域之间没有重排序,下图是一种可能的执行时序:
在上图中,写普通域 i 的操作被编译器重排序到了构造函数之外,读线程 B 错误的读取了普通变量 i 初始化之前的值。
而写 final 域的操作,被写 final 域的重排序规则 “限定” 在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。
2)规则描述
写 final 域的重排序规则如下:
JMM (Java 内存模型)禁止编译器把 final 域的写重排序到构造函数之外。
编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。
写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程 B “看到” 对象引用 obj 时,很可能 obj 对象还没有构造完成(对普通域 i 的写操作被重排序到构造函数外,此时初始值 2 还没有写入普通域 i)。
2.2、读 final 域的重排序规则
1)分析
初次读对象引用与初次读该对象包含的 final 域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。
大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如 alpha 处理器),这个规则就是专门用来针对这种处理器。
reader () 方法包含三个操作:
1 | public static void reader() { |
假设写线程 A 没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:
在上图中,读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没有被写线程 A 写入,这是一个错误的读取操作。
而读 final 域的重排序规则会把读对象 final 域的操作 “限定” 在读对象引用之后,此时该 final 域已经被 A 线程初始化过了,这是一个正确的读取操作。
2)规则描述
读 final 域的重排序规则如下:
在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。
编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。
读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。在这个示例程序中,如果该引用不为 null,那么引用对象的 final 域一定已经被 A 线程初始化过了。
2.3、如果 final 域是引用类型
1 | public class FinalReferenceExample { |
1)分析
假设首先线程 A 执行 writerOne () 方法,执行完后线程 B 执行 writerTwo () 方法,执行完后线程 C 执行 reader () 方法。下面是一种可能的线程执行时序:
线程 A :obj = new FinalReferenceExample()
主要包括:
1 是写 final 引用:intArray = new int[1];
2 是写 final 引用对象的成员域:intArray[0] = 1;
3 是把构造对象的引用赋值给引用变量 obj 。
这里除了前面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序。
JMM 可以确保读线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。即 C 至少能看到数组下标 0 的值为 1。而写线程 B 对数组元素的写入,读线程 C 可能看的到,也可能看不到。JMM 不保证线程 B 的写入对读线程 C 可见,因为写线程 B 和读线程 C 之间存在数据竞争,此时的执行结果不可预知。
如果想要确保读线程 C 看到写线程 B 对数组元素的写入,写线程 B 和读线程 C 之间需要使用同步原语(lock 或 volatile)来确保内存可见性。
2)描述
- 在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
三、为什么 final 引用不能从构造函数内”逸出“
写 final 域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的 final 域已经在构造函数中被正确初始化过了。
其实要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中 “逸出”。
为了说明问题,让我们来看下面示例代码:
1 | public class FinalReferenceEscapeExample { |
假设一个线程 A 执行 writer () 方法,另一个线程 B 执行 reader () 方法。
这里的操作 2(obj = this
)使得对象还未完成构造前就为线程 B 可见。即使这里的操作 2 是构造函数的最后一步,且即使在程序中操作 2 排在操作 1 后面,执行 read () 方法的线程仍然可能无法看到 final 域被初始化后的值,因为这里的操作 1 和操作 2 之间可能被重排序。
实际的执行时序可能如下图所示:
从上图可以看出:在构造函数返回前,被构造对象的引用不能为其他线程可见(不能将 final 引用在构造函数内逸出),因为此时的 final 域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到 final 域正确初始化之后的值。
四、final 语义
通过为 final 域增加写和读重排序规则,可以为 java 程序员提供初始化安全保证:
只要对象是正确构造的(被构造对象的引用在构造函数中没有 “逸出”),那么不需要使用同步(指 lock 和 volatile 的使用),就可以保证任意线程都能看到这个 final 域在构造函数中被初始化之后的值。
final 关键字的可见性是指::被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。
《Java并发编程的艺术》