1. 程式人生 > >Java中final修飾符的初始化安全性的理解

Java中final修飾符的初始化安全性的理解

今天看《Java併發程式設計實戰》看到安全釋出的問題中final修飾符的作用,一時半會沒有看明白,查了一些資料才懂了一些深層次的原因,所以做一些記錄。

首先我們來看一下書中的例子和描述

//不安全的釋出
public Holder holder;

public void initialize() {
    holder = new Holder(42);
}

這段是一個不安全的物件釋出,書上的描述是“由於存在可見性問題,其他執行緒看到的Holder物件將處於不一致的狀態,即便在該物件的建構函式中已經正確地構建了不變性條件。這種不正確的釋出導致其他執行緒看到尚未建立完成的物件。”這段我讀到的時候的理解就是holder由於是完全可見的,那有可能沒有執行建構函式就被使用了,自然會出錯嘛…可是越往後讀越證明,我想的太天真了。

隨後書中舉了一個簡單的例子證明上面這段程式碼是不安全的釋出

public class Holder {
  private int n;

  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n != n)
      throw new AssertionError("This statement is false.");
  }
}
書上是這麼描述問題的,另一個執行緒呼叫assertSanity時有可能丟擲異常,看到這裡我覺得哎呀我想對了,你的holder是個空引用之類的不可預知狀態,當然會報錯了!可是當看到書中的解釋,我就懵逼了…

“由於沒有使用同步來確保Holder物件對其他執行緒可見,因此將Holder稱為“未被正確釋出”。在未被正確釋出的物件中存在兩個問題。
首先 ,除了釋出物件的執行緒外,其他執行緒可以看到的 Holder域是一個失效值 ,因此將看到一個空引用或者之前的舊值。
然而 ,更糟糕的情況是,執行緒看到Holder引用的值是最新的,但Holder狀態的值卻是失效的。情況變得更加不可預測的是,某個執行緒在第一次讀取域時得到失效值,而再次讀取這個域時會得到一個更新值,這也是assertSainty丟擲AssertionError的原因。”

上面說了兩個問題,第一種就是我們設想的最簡單的情況(還是有悟性的哈哈),可當我看到第二種,我就懵逼了,特別是這句”執行緒看到Holder引用的值是最新的,但Holder狀態的值卻是失效的“,我的引用都有效了,狀態為何是失效的呢?(覺得簡單的大牛不要嘲笑我,一開始看到這裡真的懵)

看不懂歸看不懂,抱著往下看看我說不定就明白了的心態我硬著頭皮往下看,可誰知,越往下看越懵逼,書中說了一種解決問題的方式:“如果將n宣告為final型別,那麼Holder將不可變,那麼即使Holder沒有被正確的釋出,在assertSanity中也不會丟擲異常”,這……本來就不知道為何會又第二種異常,這個解決方法就更不明白了,這就加了個小小的final,就不會有問題了,這簡直是一環套一環的懵逼。

好了我的疑惑已經說的很明白了:

第一、我不知道為什麼在holder引用最新的情況下會報出異常

第二、我不知道為什麼加了final修飾n之後就不會出現問題

下面我就開始解惑了

第一、為什麼在holder引用最新的情況下會報出異常

我們來看一下我們認為的過程,
    holder = new Holder(42);
我們認為holder是最新的,就意味著holder指向了我們所建立的新的物件,也就是n為42的物件對吧,既然引用已經指向了,那我呼叫holder的函式難道還會有問題嘛?這裡這個問題在我前一篇講單例模式在併發中的實現的部落格中已經提到了,我們認為的執行過程是怎麼樣的呢? (1)給Holder物件分配記憶體 (2)呼叫Holder的建構函式,也就是給n賦值的過程,初始化了成員欄位 (3)將holder引用指向我們分配的記憶體空間

這就是這句程式碼做的基本步驟,我們認為既然第3步已經執行了(holder引用已經指向最新了),那1、2步肯定已經完成了,可是,在JVM自身的效能優化中,是允許這個順序亂序執行的,也就是說,它不能保證執行的順序是1、2、3,也有可能是1、3、2等等很多種情況,這就是問題所在,假設執行了1、3,這是引用已經是最新的了,但2的建構函式沒有執行,那你的物件的狀態值就是失效的,就是說你的n是失效值,當這種偶然的情況出現,另一個執行緒呼叫assertSanity自然會報出異常,這就解釋了為什麼holder引用最新的情況下會報出異常。

第二、為什麼加了final修飾n之後就不會出現問題

知道了上面程式碼的問題所在,我們就可以來了解一下final的作用,javamex上也有一篇“Thread-safety with the Java final keyword”,其中有一段對於final作用的解釋是這麼說的

“The final field is a means of what is sometimes called safe publication . Here, "publication" of an object means creating it in one thread and then having that newly-created object be referred to by another thread at some point in the future. When the JVM executes the constructor of your object, it must store values into the various fields of the object, and store a pointer to the object data. As in any other case of data writes, these accesses can potentially occur out of order, and their application to main memory can be delayed and other processors can be delayed unless you take special steps to combat this. In particular, the pointer to the object data could be stored to main memory and accessed before the fields themselves have been committed (this can happen partly because of compiler ordering: if you think about how you'd write things in a low-level language such as C or assembler, it's quite natural to store a pointer to a block of memory, and then advance the pointer as you're writing data to that block). And this in turn could lead to another thread seeing the object in an invalid or partially constructed state.
    final prevents this from happening: if a field is final , it is part of the JVM specification that it must effectively ensure that, once the object pointer is available to other threads, so are the correct values of that object's final fields.”

大體的意思很簡單,對於含有final域的物件,JVM必須保證對物件的初始引用在建構函式之後執行,不能亂序執行(out of order),也就是可以保證一旦你得到了引用,final域的值都是完成了初始化的,也就是書中所說的“初始化安全性”的保證。這樣一來,我們就可以理解為什麼上面的問題就可以被解決了,JVM保證了不會亂序執行,自然也不會出現問題。

相關推薦

Javafinal修飾初始安全性理解

今天看《Java併發程式設計實戰》看到安全釋出的問題中final修飾符的作用,一時半會沒有看明白,查了一些資料才懂了一些深層次的原因,所以做一些記錄。 首先我們來看一下書中的例子和描述 //不安全的釋出 public Holder holder; public void

Javafinal修飾(6.4)

final關鍵字可用於修飾類,變數和方法。當final修飾變數時,表示該變數一旦獲得初始值就不能重新被賦值。 1. final成員變數 對於final修飾的成員變數而言,一旦有了初始值,就不能被重賦值,如果既沒有在定義成員變數時指定初始值,也沒有在初始化塊,構造器中為成員變數指定初始值,

Javafinal修飾對不同變數的不同影響

final修飾符可以用來修飾類、方法和變數,用於表示它修飾的類、方法和變數不可改變。final修飾變數時,表示該變數一旦獲得了初始值就不可被改變。 由於final變數獲取初始值之後就不能重新賦值,所以final修飾成員變數和區域性變數時有一定程度的不同。 final

javafinal修飾的使用方法

成員變數是隨類初始化或物件初始化的.當類初始化時,系統會為該類的類變數分配記憶體,並分配預設值;當建立物件時,系統會為該物件的例項變數分配記憶體,並分配預設值.也就是說.當執行靜態初始化塊時可以對類變數賦初始值;當執行普通初始化塊,構造時可對例項變數賦初始值.因此,成員變數的初始值可以在定義該變數時指定預

java修飾final和static

1.final 修飾類時表明該類不能被繼承,自然類中的方法預設是final型的。 2.final 修飾方法時不允許被子類覆蓋,也就是可以被繼承。一個final類中,一個final方法只能被實現一次。 public class Test1 { public fi

Java基礎(二)----------JavaStatic修飾final關鍵字

1.Static靜態修飾符 在程式中任何變數或者程式碼都是在編譯時,由系統自動分配記憶體來儲存的,而所謂靜態就是指在編譯後分配的記憶體會一直存在,直到程式退出時才會釋放記憶體空間。Java 中被 static 修飾的成員稱為靜態成員或類成員。它屬於整個類所有,而不是某個物件所有,即被類的所有物件

javaVolatile修飾的含義

線程 代碼 sync 一個 vol tracking ava 變量 拷貝 在java語言中:為了獲得最佳速度,同意線程保存共享成員變量的私有拷貝。並且僅僅當線程進入或者離開同步代碼塊時才與共享成員變量的原始值進行對照。

java訪問修飾

addclass ext pri post span 沒有 pretty pub () 較之c++ 中 public,proctected, private 三種訪問控制, java多了默認訪問控制。 java中四種訪問控制權限 簡單描寫敘述為一下四

Java修飾及其作用

java修飾符修飾符類型修飾符說明訪問控制修飾符defaultdefault (即缺省,什麽也不寫): 在同一包內可見,不使用任何修飾符。使用對象:類、接口、變量、方法。privateprivate : 在同一類內可見。使用對象:變量、方法。 註意:不能修飾類(外部類)publicpublic : 對所有

JavaFinal修飾一個變數時,是引用不能變還是引用的物件不能變

Java中,使用Final修飾一個變數,是引用不能變,還是引用物件不能變?   是引用物件的地址不能變,引用變數所指的物件的內容可以改變。   final變數永遠指向這個物件,是一個常量指標,而不是指向常量的指標。   比如: final StringBuffer sb=new Stri

javafinal修飾方法傳入引數的含義

final型別修飾的引數分為兩種型別 基本型別 與引用型別  引數加final 1、斯坦福教授說的好,方法可以比喻成一臺機器(麵包機),沒錯,在我的機器(方法)裡面,我要的引數加了final(要原料),你給我傳過來的引數,機器裡面就無法改了,也就是說在機器裡的這個引數,一直指向的都

Javastatic修飾的作用

近日在做網路通訊伺服器和客戶端的Java語言開發,碰到了獲取客戶端長連線Channel物件,利用唯一物件Client的獲取方法getSingleClientInstance(),以為可以得到,但是顯示始終為空指標。之後又換了好幾種獲取值的辦法,一直是空指標。最後想著static修飾符的作用不

Java的null和初始

最近在學習Java8的過程中,在寫行為引數化以及lambad表示式的例子中,一個將陣列中的偶數輸出儲存到一個ArrayList中,出現的問題? 具體程式碼如下: package com.dong.j

Javavolatile修飾理解

由於CPU的執行速度要高於記憶體讀取資料的速度,所以將需要運算的資料複製一份到CPU的快取記憶體中,也就是給當前執行執行緒的執行記憶體中放入副本。運算結束後再將高速緩衝中的資料重新整理到主存中。 引出問題 在併發環境下,資料運算之後重新整理到主存的時間是不確定的,所以會導致其

Java訪問修飾作用範圍

Java中類的訪問許可權修飾符有private、default、protected、public,以下來分別介紹: (1)私有許可權(private) private可以修飾資料成員、構造方法及方法成員,不可以修飾類。被他修飾的成員,只能在定義他們的類中使用,在其他類中不能

C和Javastatic修飾的作用

C中的static C語言中的static作用有兩個:其一,增加區域性變數的生命週期,將其升級為全域性變數;其二,宣告變數或常量不可被其他檔案直接引用,必須通過標頭檔案包含的方式。 --------

java訪問修飾關鍵字的區別

public、protected、private以及預設default(不寫)  作用: 用來修飾類(介面、抽象類)、方法、屬性、構造方法、常量、主函式 類的成員不寫訪問修飾符時時預設default,預設情況對於同一個包而言等同於public 子類使用是需要繼承

Java字串定義,初始,賦值為null的區別

1、概述:字串定義 只定義不分配記憶體空間,不做任何操作;字串初始化 兩種方式直接等號賦值,用new初始化,直接等號賦值放入記憶體池,其它變數也可以引用;new初始化分配記憶體空間,不可引用;字串賦值為

javastatic修飾的程式碼的載入順序

1:首先載入被final static 修飾的原始資料型別的資料成員(若是其他型別的物件,包括原始資料型別的類包裝器,如Integer,也排在第3載入)。 2:其次載入處於static塊中的程式碼塊。 3:最後才是載入只被static修飾的資料成員。 測試例項: pack

javaMap和List初始的兩種方法

第一種方法(常用方法): //初始化List List<string> list = new ArrayList</string><string>