final關(guān)鍵字特性
final關(guān)鍵字在java中使用非常廣泛,可以申明成員變量、方法、類(lèi)、本地變量。一旦將引用聲明為final,將無(wú)法再改變這個(gè)引用。final關(guān)鍵字還能保證內(nèi)存同步,本博客將會(huì)從final關(guān)鍵字的特性到從java內(nèi)存層面保證同步講解。這個(gè)內(nèi)容在面試中也有可能會(huì)出現(xiàn)。
final使用
final變量
final變量有成員變量或者是本地變量(方法內(nèi)的局部變量),在類(lèi)成員中final經(jīng)常和static一起使用,作為類(lèi)常量使用。其中類(lèi)常量必須在聲明時(shí)初始化,final成員常量可以在構(gòu)造函數(shù)初始化。
public class Main {
public static final int i; //報(bào)錯(cuò),必須初始化 因?yàn)槌A吭诔A砍刂芯痛嬖诹?,調(diào)用時(shí)不需要類(lèi)的初始化,所以必須在聲明時(shí)初始化
public static final int j;
Main() {
i = 2;
j = 3;
}
}
就如上所說(shuō)的,對(duì)于類(lèi)常量,JVM會(huì)緩存在常量池中,在讀取該變量時(shí)不會(huì)加載這個(gè)類(lèi)。
public class Main {
public static final int i = 2;
Main() {
System.out.println("調(diào)用構(gòu)造函數(shù)"); // 該方法不會(huì)調(diào)用
}
public static void main(String[] args) {
System.out.println(Main.i);
}
}
final方法
final方法表示該方法不能被子類(lèi)的方法重寫(xiě),將方法聲明為final,在編譯的時(shí)候就已經(jīng)靜態(tài)綁定了,不需要在運(yùn)行時(shí)動(dòng)態(tài)綁定。final方法調(diào)用時(shí)使用的是invokespecial指令。
class PersonalLoan{
public final String getName(){
return"personal loan”;
}
}
class CheapPersonalLoan extends PersonalLoan{
@Override
public final String getName(){
return"cheap personal loan";//編譯錯(cuò)誤,無(wú)法被重載
}
public String test() {
return getName(); //可以調(diào)用,因?yàn)槭莗ublic方法
}
}
final類(lèi)
final類(lèi)不能被繼承,final類(lèi)中的方法默認(rèn)也會(huì)是final類(lèi)型的,java中的String類(lèi)和Integer類(lèi)都是final類(lèi)型的。
final class PersonalLoan{}
class CheapPersonalLoan extends PersonalLoan { //編譯錯(cuò)誤,無(wú)法被繼承
}
final關(guān)鍵字的知識(shí)點(diǎn)
- final成員變量必須在聲明的時(shí)候初始化或者在構(gòu)造器中初始化,否則就會(huì)報(bào)編譯錯(cuò)誤。final變量一旦被初始化后不能再次賦值。
- 本地變量必須在聲明時(shí)賦值。 因?yàn)闆](méi)有初始化的過(guò)程
- 在匿名類(lèi)中所有變量都必須是final變量。
- final方法不能被重寫(xiě), final類(lèi)不能被繼承
- 接口中聲明的所有變量本身是final的。類(lèi)似于匿名類(lèi)
- final和abstract這兩個(gè)關(guān)鍵字是反相關(guān)的,final類(lèi)就不可能是abstract的。
- final方法在編譯階段綁定,稱(chēng)為靜態(tài)綁定(static binding)。
- 將類(lèi)、方法、變量聲明為final能夠提高性能,這樣JVM就有機(jī)會(huì)進(jìn)行估計(jì),然后優(yōu)化。
final方法的好處:
- 提高了性能,JVM在常量池中會(huì)緩存final變量
- final變量在多線程中并發(fā)安全,無(wú)需額外的同步開(kāi)銷(xiāo)
- final方法是靜態(tài)編譯的,提高了調(diào)用速度
- final類(lèi)創(chuàng)建的對(duì)象是只可讀的,在多線程可以安全共享
從java內(nèi)存模型中理解final關(guān)鍵字
java內(nèi)存模型對(duì)final域遵守如下兩個(gè)重拍序規(guī)則
- 初次讀一個(gè)包含final域的對(duì)象的引用和隨后初次寫(xiě)這個(gè)final域,不能重拍序。
- 在構(gòu)造函數(shù)內(nèi)對(duì)final域?qū)懭耄S后將構(gòu)造函數(shù)的引用賦值給一個(gè)引用變量,操作不能重排序。
以上兩個(gè)規(guī)則就限制了final域的初始化必須在構(gòu)造函數(shù)內(nèi),不能重拍序到構(gòu)造函數(shù)之外,普通變量可以。
具體的操作是
- java內(nèi)存模型在final域?qū)懭牒蜆?gòu)造函數(shù)返回之前,插入一個(gè)StoreStore內(nèi)存屏障,靜止處理器將final域重拍序到構(gòu)造函數(shù)之外。
- java內(nèi)存模型在初次讀final域的對(duì)象和讀對(duì)象內(nèi)final域之間插入一個(gè)LoadLoad內(nèi)存屏障。
new一個(gè)對(duì)象至少有以下3個(gè)步驟
- 在堆中申請(qǐng)一塊內(nèi)存空間
- 對(duì)象進(jìn)行初始化
- 將內(nèi)存空間的引用賦值給一個(gè)引用變量,可以理解為調(diào)用invokespecial指令
普通成員變量在初始化時(shí)可以重排序?yàn)?-3-2,即被重拍序到構(gòu)造函數(shù)之外去了。 final變量在初始化必須為1-2-3。
讀寫(xiě)final域重拍序規(guī)則
public class FinalExample {
int i;
final int j;
static FinalExample obj;
public void FinalExample () {
i = 1; // 1
j = 2; // 2
}
public static void writer () { //寫(xiě)線程A
obj = new FinalExample (); // 3
}
public static void reader () { //讀線程B執(zhí)行
if(obj != null) { //4
int a = object.i; //5
int b = object.j; //6
}
}
}
我們可以用happens-before來(lái)分析可見(jiàn)性。結(jié)果是保證a讀取到的值可能為0,或者1 而b讀取的值一定為2。
首先,由final的重拍序規(guī)則決定3HB2,但是3和1不存在HB關(guān)系,原因在上面說(shuō)過(guò)了。 因?yàn)榫€程B在線程A之后執(zhí)行,所以3HB4。
那么2和4的HB關(guān)系怎么確定?? final的重拍序規(guī)則規(guī)定final的賦值必須在構(gòu)造函數(shù)的return之前。所以2HB4。因?yàn)樵谝粋€(gè)線程內(nèi)4HB6.所以可以得出結(jié)論2HB5。則b一定能得到j(luò)的最新值。而a就不一定了,因?yàn)闆](méi)有HB關(guān)系,可以讀到任意值。
HB判斷可見(jiàn)性關(guān)系真是太方便了??梢詤⒖嘉业牧硗庖粋€(gè)博客http://medesqure.top/2018/08/25/happen-before/
可能發(fā)生的執(zhí)行時(shí)序如下所示。

final對(duì)象是引用類(lèi)型
如果final域是一個(gè)引用類(lèi)型,比如引用的是一個(gè)int類(lèi)型的數(shù)組。對(duì)于引用類(lèi)型,寫(xiě)final域的重拍序規(guī)則增加了如下的約束
- 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final引用的對(duì)象的成員域的寫(xiě)入和隨后在構(gòu)造函數(shù)外將被構(gòu)造對(duì)象的引用賦值給引用變量之間不能重拍序。 即先寫(xiě)int[]數(shù)組的內(nèi)容,再將引用拋出去。
public class FinalReferenceExample {
final int[] intArray; //final是引用類(lèi)型
static FinalReferenceExample obj;
public FinalReferenceExample () { //構(gòu)造函數(shù) 在構(gòu)造函數(shù)中不能被重排序 final類(lèi)型在聲明或者在構(gòu)造函數(shù)中要賦值。
intArray = new int[1]; //1
intArray[0] = 1; //2
}
public static void writerOne () { //寫(xiě)線程A執(zhí)行
obj = new FinalReferenceExample (); //3
}
public static void writerTwo () { //寫(xiě)線程B執(zhí)行
obj.intArray[0] = 2; //4
}
public static void reader () { //讀線程C執(zhí)行
if (obj != null) { //5
int temp1 = obj.intArray[0]; //6
}
}
}
JMM保證了3和2之間的有序性。同樣可以使用HB原則去分析,這里就不分析了。執(zhí)行順序如下所示。

final引用不能從構(gòu)造函數(shù)“逸出”
JMM對(duì)final域的重拍序規(guī)則保證了能安全讀取final域時(shí)已經(jīng)在構(gòu)造函數(shù)中被正確的初始化了。
但是如果在構(gòu)造函數(shù)內(nèi)將被構(gòu)造函數(shù)的引用為其他線程可見(jiàn),那么久存在對(duì)象引用在構(gòu)造函數(shù)中逸出,final的可見(jiàn)性就不能保證。 其實(shí)理解起來(lái)很簡(jiǎn)單,就是在其他線程的角度去觀察另一個(gè)線程的指令其實(shí)是重拍序的。
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample () {
i = 1; //1寫(xiě)final域
obj = this; //2 this引用在此“逸出” 因?yàn)閛bj不是final類(lèi)型的,所以不用遵守可見(jiàn)性 }
public static void writer() {
new FinalReferenceEscapeExample ();
}
public static void reader {
if (obj != null) { //3
int temp = obj.i; //4
}
}
}
操作1的和操作2可能被重拍序。在其他線程觀察時(shí)就會(huì)訪問(wèn)到未被初始化的變量i,可能的執(zhí)行順序如圖所示。

本文結(jié)束,歡迎閱讀。
本人博客 http://medesqure.top/ 歡迎觀看