Final关键字特点 4.1 Final变量
Final变量有成员变量或局部变量(方法内的局部变量),final常在类成员中作为类常量一起使用。 其中,类常量必须在声明时初始化,而final成员常量可以在构造函数中初始化。
最后一堂课
Final类不能被继承,final类中的方法也默认是final的。 java中的类和类都是final的。
最终方法的好处:
提高了性能,JVM会将final变量缓存在常量池中final变量在多线程中并发安全,无需额外的同步开销final方法是静态编译的,提高了调用速度*final类创建的对象是只读的,在多个线程之间共享空白final是安全的
变量或引用被final修饰,但没有被初始化。 必须在构造函数中初始化,并且只能出现一个构造函数。
Final的内置机制:
除了函数本身的执行时间外,调用函数还需要额外的时间来查找函数(类内部有函数签名和函数地址的映射表); 所以减少函数调用次数就相当于减少性能消耗。 这是内置机制的一个实现原理。
让我举一个例子:
public class Test{
final void func(){
System.out.println("g");
}
public void main(String[] args){
for(int j=0;j<1000;j++)
func();
}
}
经过编译器优化之后,这个类变成了相当于这样写:
public class Test{
final void func(){System.out.println("g");};
public void main(String[] args){
for(int j=0;j<1000;j++)
{
System.out.println("g");
}
}
}
使用 Final 修饰符是否可以提高速度和效率?
添加总比不添加好
使用final修饰变量是否会使变量的值不可更改?
Final关键字只能保证变量本身不能被赋予新值,但不能保证变量的内部结构不会被修改。 例如,如果有如下代码 Color.color = new[]{""}; 在main方法中,会报错。
public class Final
{
public static void main(String[] args)
{
Color.color[3] = "white";
for (String color : Color.color)
System.out.print(color+" ");
}
}
class Color
{
public static final String[] color = { "red", "blue", "yellow", "black" };
}
执行结果:
red blue yellow white
看!,黑色变成了白色。
如何保证数组内部不被修改
import java.util.AbstractList;
import java.util.List;
public class Final
{
public static void main(String[] args)
{
for (String color : Color.color)
System.out.print(color + " ");
Color.color.set(3, "white");
}
}
class Color
{
private static String[] _color = { "red", "blue", "yellow", "black" };
public static List color = new AbstractList()
{
@Override
public String get(int index)
{
return _color[index];
}
@Override
public String set(int index, String value)
{
throw new RuntimeException("为了代码安全,不能修改数组");
}
@Override
public int size()
{
return _color.length;
}
};
}
Final方法的三个规则与jvm的关系
与前面介绍的锁和和相比,对final字段的读写更像是普通的变量访问。 对于最终字段,编译器和处理器遵循两个重新排序规则:
写入构造函数中的 Final 字段并随后将对构造对象的引用分配给引用变量无法重新排序。 对包含最终字段的对象的引用的初始读取与该最终字段的后续初始读取之间不能存在重新排序。
下面,我们用一些示例代码来说明这两个规则:
public class FinalExample {
int i; // 普通变量
final int j; //final 变量
static FinalExample obj;
public void 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 域
}
}
这里假设一个线程A执行()方法,然后另一个线程B执行()方法。 下面我们通过这两个线程的交互来说明这两个规则。
为最终字段编写重新排序规则
写入最终字段的重新排序规则禁止在构造函数之外重新排序写入最终字段。 本规则的实施包括以下两个方面:
现在我们来分析一下()方法。 ()方法只包含一行代码:=new()。 这行代码由两步组成:
构造一个类型的对象; 将此对象的引用分配给引用变量 obj。
假设线程B读取对象引用和读取对象字段之间没有重新排序(为什么需要这个假设将很快解释),下图是可能的执行顺序:
上图中,写入普通字段的操作在构造函数之外被编译器重新排序,读取线程B在初始化之前错误地读取了普通变量i的值。 然而,写入final字段的操作在构造函数内受到写入final字段的重排序规则的“限制”,读取线程B在初始化后正确读取了final变量的值。
编写final字段的重排序规则可以保证在对象引用对任何线程可见之前,对象的final字段已经被正确初始化,而普通字段则没有这个保证。 以上图为例,当读取线程B“看到”引用obj的对象时,很可能obj对象还没有被构造出来(对公共字段i的写操作在构造函数之外重新排序,并且初始此时值2尚未写入公共域i)。
阅读最终字段的重新排序规则
读取final字段的重新排序规则如下:
对象引用的初始读取和对象中包含的最终字段的初始读取之间存在间接依赖性。 由于编译器尊重间接依赖性,因此编译器不会重新排序这两个操作。 大多数处理器也会尊重间接依赖性,并且大多数处理器不会重新排序这两个操作。 但是,有一些处理器允许对具有间接依赖性的操作进行重新排序(例如 alpha 处理器),并且此规则是专门为此类处理器设计的。
()方法包含三个操作:
第一次读取引用的是变量obj; 第一个读取引用指向对象公共字段 j 的变量 obj。 初始读取的引用变量obj指向对象的最终字段i。
现在我们假设写入线程 A 中没有发生重新排序,并且程序正在不遵守间接依赖关系的处理器上执行。 这是一个可能的执行顺序:
在上图中,读取对象普通字段的操作在读取对象引用之前由处理器重新排序。 当读取一个公共域时,该域还没有被写入线程A写入过,这是一个错误的读操作。 然而,读取final字段的重排序规则会“限制”在读取对象引用之后读取对象的final字段的操作。 此时final字段已经被线程A初始化了,这是一次正确的读操作。
读取final字段的重排序规则可以保证:在读取对象的final字段之前,必须先读取包含final字段的对象的引用。 在此示例程序中,如果引用不为 null,则引用对象的 Final 字段一定已由线程 A 初始化。
如果最终字段是引用类型
上面我们看到的final字段是基本数据类型,我们看看如果final字段是引用类型会发生什么?
看一下下面的示例代码:
public class FinalReferenceExample {
final int[] intArray; //final 是引用类型
static FinalReferenceExample obj;
public FinalReferenceExample () { // 构造函数
intArray = new int[1]; //1
intArray[0] = 1; //2
}
public static void writerOne () { // 写线程 A 执行
obj = new FinalReferenceExample (); //3
}
public static void writerTwo () { // 写线程 B 执行
obj.intArray[0] = 2; //4
}
public static void reader () { // 读线程 C 执行
if (obj != null) { //5
int temp1 = obj.intArray[0]; //6
}
}
}
这里的final字段是一个引用类型,它引用的是一个int类型的数组对象。 对于引用类型,编写最终字段的重新排序规则对编译器和处理器施加以下约束:
写入构造函数内的最终引用对象的字段,然后将对构造对象的引用分配给构造函数外部的引用变量,无法重新排序。
对于上面的示例程序,我们假设线程A先执行()方法,然后线程B执行后执行()方法,线程C执行后执行()方法。 以下是一个可能的线程执行时序:
上图中,1是写入final字段,2是写入final字段引用的对象的字段,3是将构造对象的引用赋值给引用变量。 除上述1不能与3重新排序外,2和3也不能重新排序。
JMM可以保证读取线程C至少可以在构造函数中看到写入线程A对最终引用对象的字段的写入。 也就是说,C至少可以看到数组的下标0的值为1。当写线程B写入数组元素时,读线程C可能看到也可能看不到。 JMM不保证线程B的写入对读取线程C可见,因为写入线程B和读取线程C之间存在数据竞争,此时的执行结果是不可预测的。
如果要确保读取线程 C 看到写入线程 B 对数组元素的写入,则需要在写入线程 B 和读取线程 C 之间使用同步原语(锁或 )来确保内存可见性。
为什么最终引用不能从构造函数内部“逃逸”
前面我们提到,编写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
}
}
}
假设一个线程A执行()方法,另一个线程B执行()方法。 这里的操作 2 使对象在完成构造之前对线程 B 可见。 即使这里的操作2是构造函数的最后一步,并且即使程序中操作2安排在操作1之后,执行read()方法的线程仍然可能看不到final字段初始化后的值,因为这里的操作1和操作2可能会被重新排序。 实际执行时序可能如下图所示:
从上图我们可以看出,在构造函数返回之前,构造对象的引用是无法被其他线程看到的,因为此时final字段可能还没有被初始化。 构造函数返回后,任何线程都保证可以看到最终字段的正确初始化值。
处理器中最终语义的实现
现在我们以x86处理器为例来说明final语义在处理器中的具体实现。
我们上面提到,编写 Final 字段的重新排序规则将要求编译器在编写 Final 字段之后、构造函数之前插入一个屏障。 读取最终字段的重新排序规则要求编译器在读取最终字段的操作之前插入屏障。
由于 x86 处理器不会重新排序写入操作,因此在 x86 处理器上省略了写入最终字段所需的屏障。 此外,由于 x86 处理器不会对具有间接依赖性的操作进行重新排序,因此 x86 处理器中也省略了读取最终字段所需的屏障。 也就是说,在x86处理器中,final字段的读/写不会插入任何内存屏障!
为什么要增强final的语义
旧 Java 内存模型中最严重的缺陷之一是线程可以看到最终字段更改的值。 例如,一个线程当前看到一个整型final字段的值为0(初始化前的默认值),过了一段时间,当该线程读取final字段的值时,发现该值变成了1 (线程初始化后的某个值)。 最常见的例子是,在旧的Java内存模型中, 的值可能会发生变化(参考资料2中有具体的例子,有兴趣的读者可以自行参考,这里不再赘述)。
排序,因此在 x86 处理器上,读取最终字段所需的屏障也被省略。 也就是说,在x86处理器中,final字段的读/写不会插入任何内存屏障!
为什么要增强final的语义
旧 Java 内存模型中最严重的缺陷之一是线程可以看到最终字段更改的值。 例如,一个线程当前看到一个整型final字段的值为0(初始化前的默认值),过了一段时间,当该线程读取final字段的值时,发现该值变成了1 (线程初始化后的某个值)。 最常见的例子是,在旧的Java内存模型中, 的值可能会发生变化(参考资料2中有具体的例子,有兴趣的读者可以自行参考,这里不再赘述)。
为了修复这个漏洞,JSR-133专家组增强了final的语义。 通过为final字段添加写入和读取重排序规则,可以为java程序员提供初始化安全保证:只要正确构造了对象(构造对象的引用没有在构造函数中“逃逸”),那么就没有需要使用同步(指使用lock和),可以保证任何线程在构造函数中初始化后都可以看到final字段的值。