天天看點

【Java】你真的懂懶漢模式嗎 —— 延遲初始化

學習java單例模式或者對象建構的時候,很多人都會聽到”懶漢式“,上百度(狗頭)一搜就是一堆部落格,部落格裡還有代碼,複制粘貼一下,真香!

運氣不好的,可能複制到了以下的代碼

靜态域版本:

執行個體域版本:

以上兩段代碼都使用了雙重檢查模式 —— 檢查了兩次域是否為null

但很不幸的是,以上2段代碼都有問題,在多線程環境下都可能出錯。

閱讀下文之前,讀者可以自己思考 1 分鐘為什麼。

以第一段代碼為例,我們可以模拟一下兩條線程運作的情況:

線程a 通路了getsingletoninstance 方法,發現兩次 singletoninstance == null 檢查都是 true,于是線程a 開始初始化 singletonpattern 此處時間點設為 x

在時間點 x 線程b 通路了 getsingletoninstance 方法,此時它發現 singletoninstance 已經是非 null 了,于是高高興興地傳回了 singletoninstance 執行個體,并且外部調用開始使用 singletoninstance 的執行個體,此處時間點設為 y

問題在于:時間點 y 的 singletoninstance 執行個體真的初始化好了嗎?

由于第一段代碼裡忽略了 singletoninstance 類的其他執行個體域,我們可以假設充實一下singletoninstance 類

我們依然讓 singletonpattern 的構造器方法無需參數,但是在singletonpattern構造器方法裡初始化了兩個執行個體域。

在jvm建立一個對象執行個體時,首先會賦予這個執行個體的所有域初始值(比如 int 的初始值是 0, boolean的初始值是 false),然後再調用構造器方法。也就是說,有那麼一個時刻(比如就是時間點 y )建立的singletonpattern執行個體的 num 值為 0, really 值為 false。

此外,jvm 的指令重排可能導緻了這個對象執行個體先被指派給了變量,然後才開始初始化,是以時間點 y 變量已經不為 null,而執行個體尚未初始化結束。

正在 時間點 y,線程b已經開始使用了singletonpattern執行個體,這就是一個 bug 了,因為singletonpattern執行個體根本還沒初始化結束!

類似的,第二段懶漢式代碼一樣有這個問題。

很簡單,加個關鍵字就好了 —— volatile

靜态域版本

執行個體域版本

為什麼 volatile 可以解決以上問題?

依然以靜态域版本為例,當 volatile 修飾了singletoninstance變量之後,就會禁止jvm對這個變量相關操作做指令重排 —— 即在調用 new singletonpattern() 初始化singletonpattern執行個體時(寫操作),jvm 會保證singletonpattern執行個體完全初始化結束後才允許其他線程通路(讀操作)這個執行個體。

對于靜态域的延遲初始化, lazy initialization holder class 模式不僅性能更好,代碼可讀性也更高

不算括号,4行代碼就搞定

當getsingletoninstance()被調用時,singletonpatternholder類才第一次被jvm加載,且jvm會以線程安全的方式初始化好singletonpatternholder類的靜态域——也就是 singletonpatternholder.instance

是以 getsingletoninstance() 是線程安全的。

而執行個體域的延遲初始化的這種雙重檢查模式,除了加 volatile 關鍵字也可以嘗試把這個域改造成不可變的對象—— 原理是讀和寫一個不可變對象都是原子性的,也就不可能讀到一個初始化未完成的不可變對象。

最後談一點初學者看部落格的問題:

搜尋引擎一搜,我們的确能找到很多部落格和現成的代碼,看似很友善,但是如果不加以甄别,很可能遇到這種”有毒“的代碼。

是以,我最推崇的學習方式是 以官方文檔為主,輔以經典著作,再選擇性地看一些部落格但是要保持自己的思考。

比如我今天談到的這個延遲初始化問題,在《effective java》裡的第71條和第66條有詳細的闡述。

其他參考文檔:

the "double-checked locking is broken" declaration