1.final語義與使用
final的語義
- 編譯器做的處理編譯器可以跨同步屏障移動對final修飾的字段值進行讀取和調用任意或未知的方法對于final與non-final修飾的字段,允許編譯器保存一份final的數據緩存放在寄存器中,對比必須要加載non-final數據的情況下,它不需要從主內存中加載就可以獲取
- 並發線程下是安全的對于final修飾的字段在所有線程中是屬于不可變(基本類型值不可變,引用類型是引用地址不可變),也就是對于程序員而言,在線程中重新對final修飾的字段賦值將會編譯不通過只有在對象完全初始化之後,線程才能看到對該對象的引用,這樣就可以保證看到該對象的final字段的正確初始化值基于Happen-Before原則,程序任何對象的初始化happen-before于程序中任何其他的動作操作行爲 因此能夠保證不會被重排序,也就是說final修飾的字段在線程讀取已經先在構造器中執行寫操作 因而所有線程看到final修飾的變量均爲最終最新的版本
- final的使用模型在對象的構造函數中爲對象設置final字段;在對象的構造函數完成之前,不允許在其他線程可以看到的地方對正在構造的對象的引用執行寫操作這樣可以保證在線程看到該對象的時候,將始終看到該對象final字段的最終正確構造版本
final的線程安全性
- 源代碼
// FinalClass.java
public class FinalClass {
public final int i;
public int j;
public final DefineFinalObject defineFinalObject;
static FinalClass finalClass;
public FinalClass(){
// int x = i;
// DefineFinalObject var = defineFinalObject;
i = 4;
defineFinalObject = new DefineFinalObject();
try{
// code ... may throw exception
// i = 4;
// defineFinalObject = new DefineFinalObject();
j = 9;
// code ...
}catch (Exception e){}
}
static void writer(){
finalClass = new FinalClass();
System.out.println("have init FinalClass");
}
static void reader(){
if (finalClass != null){
int x = finalClass.i;
int y = finalClass.j;
System.out.printf("get x = %d, and y = %d", x, y);
}
}
public static void main(String[] args) {
FinalClass finalClass = new FinalClass();
System.out.println(finalClass.defineFinalObject);
finalClass.defineFinalObject.setAge(10);
System.out.println(finalClass.defineFinalObject);
}
}
- 代碼分析在構造器中,對于final修飾的基本類型/引用類型變量編譯器不允許在try中對i=4進行寫操作,會出現編譯報錯,對于沒有使用final修飾的變量j進行寫操作的j=9則沒有出現編譯報錯
- 其次,在對i=4執行的寫操作之前,編譯器不允許對final修飾的基本/引用變量進行讀操作,否則編譯報錯基于上述編譯器的規則,最終保證final的基本類型/引用變量是在其他線程是最終最新的版本,也就是i=4以及defineFinalObject = new DefineFinalObject()創建對象並引用對應的對象地址
- 在main的線程方法中,可以對不可變的defineFinalObject的屬性信息進行修改,說明引用類型不可變是指對應的對象內存地址,即使無法再通過defineFinalObject = new DefineFinalObject()的方式重新指向一個新的引用地址
- 最後一點就是,final必須是在構造器中完成初始化,同時根據Happen-Before原則,線程訪問final的數據一定是在完成初始化後的最終數據且無法再進行修改(引用類型是可以修改其屬性信息),從而保證了線程對final修飾的變量是屬于線程安全的共享數據
final與static使用分析
- 源代碼
// FinalSharedClass.java
public class FinalSharedClass {
public final static int num;
public static int x;
public final static DefineFinalObject defineFinalObject;
static {
// 靜態代碼塊,保證在加載類信息的時候也完成上述靜態數據變量的初始化賦值的操作
num = 10;
defineFinalObject = new DefineFinalObject();
System.out.printf("have finished static code for num=%d and obj=%s...\n", num, defineFinalObject);
}
{
// 默認代碼塊,等同于對象構造器,編譯器報錯,要麽是聲明被分配過要麽是上述定義的static報沒有被分配
// num = 10;
// defineFinalObject = new DefineFinalObject();
}
static void writer(){
// final 修飾的無法修改,將會編譯報錯提示已經分配值操作
// num = 20;
// defineFinalObject = new DefineFinalObject();
x = 10;
defineFinalObject.setAge(10);
}
static void reader(){
// must be same with the end of static code
System.out.printf("read final static num: %d \n", num);
System.out.printf("read final static defineFinalObject: %s \n", defineFinalObject);
// may be 0 or 10
System.out.printf("read static x: %d \n", x);
}
}
- 分析靜態代碼中的final初始化過程必須是在靜態代碼塊中,也就是加載類信息的時候同時完成對final的數據賦予值的操作在方法writer()中可以看到無法對num以及defineFinalObject再次進行寫操作,從而保證線程對于final修飾的數據只能讀取,因此並不存在線程安全問題
- 小結final且非靜態的對象變量,final將在對象構造器中完成初始化賦值操作,且不能在構造器之外執行寫操作,只能被讀取,因而不存在線程安全性問題final且爲靜態的類對象變量時,final將會在類的靜態代碼塊中完成初始化(優先于對象構造器執行),且不能在靜態代碼之外完成初始化操作,由于JVM加載類的信息的時候是優先于創建線程的,因此當線程訪問的時候final的static數據已經完成初始化賦值操作,因此也不存在線程安全問題
2. final的內存語義與實現
final的遵循的規則
- 對于final領域修飾的非static變量,對象的final領域變量的寫操作優先于該對象構造器完成初始化之後的引用賦值操作,即i=4優先于finalClass = new FinalClass();,也就是兩個操作不能重排序,final修飾的爲引用類型也是一樣遵循這個規則
- 對于final領域修飾的非static變量,對象的final領域變量在構造器初始化的讀操作優先于所有線程對該對象的final數據的讀操作,也就是構造器執行默認值i 爲默認值 0的操作優先于其他線程對i 爲 4的讀操作,也就是兩者不能重排序,同理final修飾的引用變量也是遵循這個規則
- 另外,對于final修飾且爲static的變量,在java程序中靜態代碼只執行一次,且靜態代碼完成final領域的數據變量初始化操作優先于所有線程對該變量的讀操作,相當于“寫一次讀多次”,並且寫一次是在JVM第一次創建該對象實例的時候加載的,且優先于所有線程的其他行爲動作,對此是保證寫在前讀在後的一個邏輯順序
final的內存語義是如何實現的
- aarch架構內存屏障指令
// A more convenient access to dmb for our purposes
enum Membar_mask_bits {
StoreStore = ISHST,
LoadStore = ISHLD,
LoadLoad = ISHLD,
StoreLoad = ISH,
AnyAny = ISH
};
- final語義也是基于內存屏障實現(aarch架構)
// templateTable_aarch64.cpp
// Issue a StoreStore barrier after all stores but before return
// from any constructor for any class with a final field. We don't
// know if this is a finalizer, so we always do so.
if (_desc->bytecode() == Bytecodes::_return)
__ membar(MacroAssembler::StoreStore);
- 分析根據上述可知,jvm在實現中由于不清楚對象什麽時候會調用finalizer方法進行回收,因此會在任何對象的構造器返回前插入內存屏障對final修飾的變量執行寫操作其次,可以看到final插入的內存屏障爲StoreStore類型,也就是在構造器返回之前插入StoreStore的內存屏障,也就是說final對變量的寫操作的可利用結果在內存屏障之前的代碼是不可用的,也就是對final x = 9的寫操作之前是看不到x=9的結果
- volatile與final寫操作的內存屏障實現區分
// templateTable_aarch64.cpp
// According to the new Java Memory Model (JMM):
// (1) All volatiles are serialized wrt to each other. ALSO reads &
// writes act as aquire & release, so:
// (2) A read cannot let unrelated NON-volatile memory refs that
// happen after the read float up to before the read. It's OK for
// non-volatile memory refs that happen before the volatile read to
// float down below it.
// (3) Similar a volatile write cannot let unrelated NON-volatile
// memory refs that happen BEFORE the write float down to after the
// write. It's OK for non-volatile memory refs that happen after the
// volatile write to float up before it.
// We only put in barriers around volatile refs (they are expensive),
// not _between_ memory refs (that would require us to track the
// flavor of the previous memory refs). Requirements (2) and (3)
// require some barriers before volatile stores and after volatile
// loads. These nearly cover requirement (1) but miss the
// volatile-store-volatile-load case. This final case is placed after
// volatile-stores although it could just as well go before
// volatile-loads.
// templateTable_arm.cpp
// StoreLoad barrier after volatile field write
volatile_barrier(MacroAssembler::StoreLoad, Rtemp);
__ b(skipMembar);
// StoreStore barrier after final field write
__ bind(notVolatile2);
volatile_barrier(MacroAssembler::StoreStore, Rtemp);
- 分析從上面可以看到volatile的寫操作內存屏障是使用StoreLoad方式,final使用的內存屏障是StoreStore方式在aarch64處理器架構中,final也可以使用與volatile相同的內存屏障
- volatile與final內存屏障僞代碼
// 針對寫操作
// Store爲寫屏障,作用就是防止重排序,同時讓數據刷新到主內存
// Load爲讀屏障,作用就是使得當前工作線程的緩存失效,直接讀取主內存數據,保證數據一致性
// for a volatile write
//
// dmb ish // store內存屏障 -- 防止重排序
// str<x> // 寫volatile數據
// dmb ish // load內存屏障 -- 保證數據一致性(目的就是要看見最新的數據)
// other codes...
// for a final write
//
// dmb ishst // store內存屏障 -- 防止重排序
// final x = 9; // 寫final數據
// dmb ishst // store內存屏障 -- 防止重排序
// other codes ...
- 結果可以看到上述描述中使用內存屏障的技術是非常昂貴的,爲了適應對應的使用場景,在java中對于volatile與final不能同時存在,同時volatile的使用場景是讀寫多,而final是一次性寫多次讀的場景,對此使用的內存屏障技術也會有所不同final建議使用爲StoreStore而不使用與volatile相同的StoreLoad內存屏障是根據使用場景來的,final實現寫一次,那麽在創建線程的時候工作內存會copy一份相同的數據作爲緩存,不需要讀取主內存的數據,同時final的寫是在構造器中完成,也就是在構造器中添加內存屏障,也保證了在對象構造器之外不能再對final的數據進行修改的操作同理,對于static的final數據,是在static代碼塊實現StoreStore內存屏障,作用和對象構造器類似
3. final規範小結
Java語言規範
- final在構造器中執行賦予值的寫操作,因此當線程訪問的時候會看到當前final修飾的變量爲最新版本的數據
- 如果在構造器函數中執行final變量的讀操作在寫操作之後,那麽會看到final分配給變量的最新數據,不存在緩存讀取
- 讀取共享變量裏的final數據,則必須先要訪問這個共享變量的引用對象然後再讀取final數據
- 通常static final表示爲常量,然而System.in/System.err/System.out也是屬于static final,是屬于遺留的原因,可通過 System.setIn, System.setOut, and System.setErr來完成賦值操作,java規範中稱之爲“寫保護”
- 對應部分源碼說明如下
//ciField.cpp
// 源碼中注釋說明
// Is this field a constant?
//
// Clarification: A field is considered constant if:
// 1. The field is both static and final
// 2. The field is not one of the special static/final
// non-constant fields. These are java.lang.System.in
// and java.lang.System.out. Abomination.
JMM規範
使用final修飾的數據在字節碼中顯示帶有ACC_FINAL的訪問標識符,對應訪問標示符號的值爲0x1000
- 使用final class XX表明該Class不能被繼承,說明該Class沒有子類
- 類的屬性字段被聲明爲final,表明該字段在對象構造器之外不能被分配值操作
- JVM規範中,volatile的訪問標識與final的訪問標識不能同時出現,也就是說在程序代碼中不能同時使用final和volatile修飾同一個變量
- 方法聲明爲final,表示該方法不能被覆蓋重寫
- 內部類使用final修飾的時候,表示在源代碼中標記或隱式結束,也就是final修飾的內部類在生成字節碼的時候內部類的標識沒有被分配,默認值爲0,一般情況下在jvm實現中沒有檢查內部類屬性與類文件的一致性