共计 5741 个字符,预计需要花费 15 分钟才能阅读完成。
final 实现原理
简介
final关键字,理论的含意就一句话,不可扭转。什么是不可扭转?就是初始化实现之后就不能再做任何的批改,润饰成员变量的时候,成员变量变成一个常数;润饰办法的时候,办法不容许被重写;润饰类的时候,类不容许被继承;润饰参数列表的时候,入参的对象也是不能够扭转。这个就是不可变,无论是援用新的对象,重写还是继承,都是扭转的办法,而 final 就是把这个变更的路给堵死
用法
final 润饰变量
- final 成员变量示意常量,只能被赋值一次,赋值后值不再扭转(final 要求地址值不能扭转)
- 当 final 润饰一个根本数据类型时,示意该根本数据类型的值一旦在初始化后便不能发生变化;
- 如果 final 润饰一个援用类型时,则在对其初始化之后便不能再让其指向其余对象了,但该援用所指向的对象的内容是能够发生变化的。实质上是一回事,因为援用的值是一个地址,final 要求值,即地址的值不发生变化。
-
final 润饰一个成员变量(属性),必须要显示初始化。这里有两种初始化形式。
- 一种是在变量申明的时候初始化。
- 第二种办法是在申明变量的时候不赋初值,然而要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
final 润饰办法
应用 final 办法的起因有两个。
- 第一个起因是把办法锁定,以防任何继承类批改它的含意,不能被重写;
- 第二个起因是效率,final 办法比非 final 办法要快,因为在编译的时候曾经动态绑定了,不须要在运行时再动静绑定。
注:类的 private 办法会隐式地被指定为 final 办法
final 润饰类
当用 final 润饰一个类时,表明这个类不能被继承。
final 类中的成员变量能够依据须要设为 final,然而要留神 final 类中的所有成员办法都会被隐式地指定为 final 办法。
在应用 final 润饰类的时候,要留神审慎抉择,除非这个类真的在当前不会用来继承或者出于平安的思考,尽量不要将类设计为 final 类。
final 关键字的益处
- final 关键字进步了性能。JVM 和 Java 利用都会缓存 final 变量。
- final 变量能够平安的在多线程环境下进行共享,而不须要额定的同步开销。
- 应用 final 关键字,JVM 会对办法、变量及类进行优化。
注意事项
- final 关键字能够用于成员变量、本地变量、办法以及类。
- final 成员变量必须在申明的时候初始化或者在结构器中初始化,否则就会报编译谬误。
- 你不可能对 final 变量再次赋值。
- 本地变量必须在申明时赋值。
- 在匿名类中所有变量都必须是 final 变量。
- final 办法不能被重写。
- final 类不能被继承。
- final 关键字不同于 finally 关键字,后者用于异样解决。
- final 关键字容易与 finalize()办法搞混,后者是在 Object 类中定义的办法,是在垃圾回收之前被 JVM 调用的办法。
- 接口中申明的所有变量自身是 final 的。
- final 和 abstract 这两个关键字是反相干的,final 类就不可能是 abstract 的。
- final 办法在编译阶段绑定,称为动态绑定(static binding)。
- 没有在申明时初始化 final 变量的称为空白 final 变量 (blank final variable),它们必须在结构器中初始化,或者调用 this() 初始化。不这么做的话,编译器会报错“final 变量 (变量名) 须要进行初始化”。
- 将类、办法、变量申明为 final 可能进步性能,这样 JVM 就有机会进行预计,而后优化。
- 依照 Java 代码常规,final 变量就是常量,而且通常常量名要大写。
- 对于汇合对象申明为 final 指的是援用不能被更改,然而你能够向其中减少,删除或者扭转内容。
原理
内存语义
写内存语义能够确保在对象的援用为任意线程可见之前,final 域曾经被初始化过了。
读内存语义能够确保如果对象的援用不为 null,则阐明 final 域曾经被初始化过了。
总之,final 域的内存语义提供了初始化平安保障。
- 写内存语义:在构造函数内对一个 final 域的写入,与随后将对象援用赋值给援用变量,这两个操作不能重排序。
- 读内存语义:首次读一个蕴含 final 域的对象的援用,与随后首次读这个 final 域,这两个操作不能重排序。
写 final 域的重排序规定
写 final 域的重排序规定禁止把 final 域的写重排序到构造函数之外。这个规定的实现蕴含上面 2 个方面:
- JMM 禁止编译器把 final 域的写重排序到构造函数之外。
- 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。
当初让咱们剖析 writer () 办法。writer () 办法只蕴含一行代码:finalExample = new FinalExample ()。这行代码蕴含两个步骤:
- 结构一个 FinalExample 类型的对象;
- 把这个对象的援用赋值给援用变量 obj。
假如线程 B 读对象援用与读对象的成员域之间没有重排序(马上会阐明为什么须要这个假如),下图是一种可能的执行时序:
在上图中,写一般域的操作被编译器重排序到了构造函数之外,读线程 B 谬误的读取了一般变量 i 初始化之前的值。而写 final 域的操作,被写 final 域的重排序规定“限定”在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。
写 final 域的重排序规定能够确保:在对象援用为任意线程可见之前,对象的 final 域曾经被正确初始化过了,而一般域不具备这个保障。以上图为例,在读线程 B“看到”对象援用 obj 时,很可能 obj 对象还没有结构实现(对一般域 i 的写操作被重排序到构造函数外,此时初始值 2 还没有写入一般域 i)。
读 final 域的重排序规定
读 final 域的重排序规定如下:
在一个线程中,首次读对象援用与首次读该对象蕴含的 final 域,JMM 禁止处理器重排序这两个操作(留神,这个规定仅仅针对处理器)。编译器会在读 final 域操作的后面插入一个 LoadLoad 屏障。
首次读对象援用与首次读该对象蕴含的 final 域,这两个操作之间存在间接依赖关系。因为编译器恪守间接依赖关系,因而编译器不会重排序这两个操作。大多数处理器也会恪守间接依赖,大多数处理器也不会重排序这两个操作。但有多数处理器容许对存在间接依赖关系的操作做重排序(比方 alpha 处理器),这个规定就是专门用来针对这种处理器。
reader() 办法蕴含三个操作:
- 首次读援用变量 obj;
- 首次读援用变量 obj 指向对象的一般域 j。
- 首次读援用变量 obj 指向对象的 final 域 i
当初咱们假如写线程 A 没有产生任何重排序,同时程序在不恪守间接依赖的处理器上执行,上面是一种可能的执行时序
在上图中,读对象的一般域的操作被处理器重排序到读对象援用之前。读一般域时,该域还没有被写线程 A 写入,这是一个谬误的读取操作。而读 final 域的重排序规定会把读对象 final 域的操作“限定”在读对象援用之后,此时该 final 域曾经被 A 线程初始化过了,这是一个正确的读取操作。
读 final 域的重排序规定能够确保:在读一个对象的 final 域之前,肯定会先读蕴含这个 final 域的对象的援用。在这个示例程序中,如果该援用不为 null,那么援用对象的 final 域肯定曾经被 A 线程初始化过了。
如果 final 域是援用类型
下面咱们看到的 final 域是根底数据类型,上面让咱们看看如果 final 域是援用类型,将会有什么成果?
请看下列示例代码:
COPYpublic 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 型的数组对象。对于援用类型,写 final 域的重排序规定对编译器和处理器减少了如下束缚:
在构造函数内对一个 final 援用的对象的成员域的写入,与随后在构造函数外把这个被结构对象的援用赋值给一个援用变量,这两个操作之间不能重排序。
对下面的示例程序,咱们假如首先线程 A 执行 writerOne() 办法,执行完后线程 B 执行 writerTwo() 办法,执行完后线程 C 执行 reader () 办法。上面是一种可能的线程执行时序:
在上图中,1 是对 final 域的写入,2 是对这个 final 域援用的对象的成员域的写入,3 是把被结构的对象的援用赋值给某个援用变量。这里除了后面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序。
JMM 能够确保读线程 C 至多能看到写线程 A 在构造函数中对 final 援用对象的成员域的写入。即 C 至多能看到数组下标 0 的值为 1。而写线程 B 对数组元素的写入,读线程 C 可能看的到,也可能看不到。JMM 不保障线程 B 的写入对读线程 C 可见,因为写线程 B 和读线程 C 之间存在数据竞争,此时的执行后果不可预知。
如果想要确保读线程 C 看到写线程 B 对数组元素的写入,写线程 B 和读线程 C 之间须要应用同步原语(lock 或 volatile)来确保内存可见性。
为什么 final 援用不能从构造函数内“逸出”
后面咱们提到过,写 final 域的重排序规定能够确保:在援用变量为任意线程可见之前,该援用变量指向的对象的 final 域曾经在构造函数中被正确初始化过了。其实要失去这个成果,还须要一个保障:在构造函数外部,不能让这个被结构对象的援用为其余线程可见,也就是对象援用不能在构造函数中“逸出”。为了阐明问题,让咱们来看上面示例代码:
COPYpublic 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 执行 writer() 办法,另一个线程 B 执行 reader() 办法。这里的操作 2 使得对象还未实现结构前就为线程 B 可见。即便这里的操作 2 是构造函数的最初一步,且即便在程序中操作 2 排在操作 1 前面,执行 read() 办法的线程依然可能无奈看到 final 域被初始化后的值,因为这里的操作 1 和操作 2 之间可能被重排序。理论的执行时序可能如下图所示:
从上图咱们能够看出:在构造函数返回前,被结构对象的援用不能为其余线程可见,因为此时的 final 域可能还没有被初始化。在构造函数返回后,任意线程都将保障能看到 final 域正确初始化之后的值。
final 语义在处理器中的实现
当初咱们以 x86 处理器为例,阐明 final 语义在处理器中的具体实现。
下面咱们提到,写 final 域的重排序规定会要求译编器在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 障屏。读 final 域的重排序规定要求编译器在读 final 域的操作后面插入一个 LoadLoad 屏障。
因为 x86 处理器不会对写 – 写操作做重排序,所以在 x86 处理器中,写 final 域须要的 StoreStore 障屏会被省略掉。同样,因为 x86 处理器不会对存在间接依赖关系的操作做重排序,所以在 x86 处理器中,读 final 域须要的 LoadLoad 屏障也会被省略掉。也就是说在 x86 处理器中,final 域的读 / 写不会插入任何内存屏障!
为什么要加强 final 的语义
在旧的 Java 内存模型中,最重大的一个缺点就是线程可能看到 final 域的值会扭转。比方,一个线程以后看到一个整形 final 域的值为 0(还未初始化之前的默认值),过一段时间之后这个线程再去读这个 final 域的值时,却发现值变为了 1(被某个线程初始化之后的值)。最常见的例子就是在旧的 Java 内存模型中,String 的值可能会扭转。
为了修补这个破绽,JSR-133 专家组加强了 final 的语义。通过为 final 域减少写和读重排序规定,能够为 java 程序员提供初始化平安保障:只有对象是正确结构的(被结构对象的援用在构造函数中没有“逸出”),那么不须要应用同步(指 lock 和 volatile 的应用),就能够保障任意线程都能看到这个 final 域在构造函数中被初始化之后的值。
final、finally、finalize 区别
- final 能够用来润饰类、办法、变量,别离有不同的意义,final 润饰的 class 代表不能够继承扩大,final 的变量是不能够批改的,而 final 的办法也是不能够重写的(override)。
- finally 则是 Java 保障重点代码肯定要被执行的一种机制。咱们能够应用 try-finally 或者 try-catch-finally 来进行相似敞开 JDBC 连贯、保障 unlock 锁等动作。
- finalize 是根底类 java.lang.Object 的一个办法,它的设计目标是保障对象在被垃圾收集前实现特定资源的回收。finalize 机制当初曾经不举荐应用,并且在 JDK 9 开始被标记为 deprecated。
本文由
传智教育博学谷狂野架构师
教研团队公布。如果本文对您有帮忙,欢送
关注
和点赞
;如果您有任何倡议也可留言评论
或私信
,您的反对是我保持创作的能源。转载请注明出处!