Java 单例模式:懒加载(延迟加载)和即时加载
Java 單例模式:懶加載(延遲加載)和即時加載
引言
在開發(fā)中,如果某個實例的創(chuàng)建需要消耗很多系統(tǒng)資源,那么我們通常會使用惰性加載機制(或懶加載、延時加載),也就是說只有當使用到這個實例的時候才會創(chuàng)建這個實例,這個好處在單例模式中得到了廣泛應用。這個機制在單線程環(huán)境下的實現(xiàn)非常簡單,然而在多線程環(huán)境下卻存在隱患。
1、單例模式的惰性加載
通常當我們設計一個單例類的時候,會在類的內(nèi)部構造這個類(通過構造函數(shù),或者在定義處直接創(chuàng)建),并對外提供一個static getInstance() 方法提供獲取該單例對象的途徑。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton(){ … } public static Singleton getInstance(){ return instance; } }這樣的代碼缺點是:第一次加載類的時候會連帶著創(chuàng)建 Singleton 實例,這樣的結果與我們所期望的不同,因為創(chuàng)建實例的時候可能并不是我們需要這個實例的時候。同時如果這個Singleton 實例的創(chuàng)建非常消耗系統(tǒng)資源,而應用始終都沒有使用 Singleton 實例,那么創(chuàng)建 Singleton 消耗的系統(tǒng)資源就被白白浪費了。
為了避免這種情況,我們通常使用惰性加載的機制,也就是在使用的時候才去創(chuàng)建。
public class Singleton{ private static Singleton instance = null; private Singleton(){ … } public static Singleton getInstance(){ if (instance == null) instance = new Singleton(); return instance; } }2、惰性加載在多線程中的問題
先將惰性加載的代碼提取出來:
public static Singleton getInstance(){ if (instance == null) instance = new Singleton(); return instance; }這是如果兩個線程 A 和 B 同時執(zhí)行了該方法,然后以如下方式執(zhí)行:
- A 進入 if 判斷,此時 foo 為 null,因此進入 if 內(nèi)
- B 進入 if 判斷,此時 A 還沒有創(chuàng)建 foo,因此 foo 也為 null,因此 B 也進入 if 內(nèi)
- A 創(chuàng)建了一個 Foo 并返回
- B 也創(chuàng)建了一個 Foo 并返回
此時問題出現(xiàn)了,我們的單例被創(chuàng)建了兩次,而這并不是我們所期望的。
3. 各種解決方案及其存在的問題
3.1 使用 Class 鎖機制
以上問題最直觀的解決辦法就是給 getInstance 方法加上一個 synchronize 前綴,這樣每次只允許一個現(xiàn)成調(diào)用 getInstance 方法:
這種解決辦法的確可以防止錯誤的出現(xiàn),但是它卻很影響性能:每次調(diào)用 getInstance 方法的時候都必須獲得 Singleton 的鎖,而實際上,當單例實例被創(chuàng)建以后,其后的請求沒有必要再使用互斥機制了
3.2 double-checked locking
曾經(jīng)有人為了解決以上問題,提出了 double-checked locking 的解決方案
讓我們來看一下這個代碼是如何工作的:首先當一個線程發(fā)出請求后,會先檢查 instance 是否為null,如果不是則直接返回其內(nèi)容,這樣避免了進入 synchronized 塊所需要花費的資源。其次,即使第2節(jié)提到的情況發(fā)生了,兩個線程同時進入了第一個 if 判斷,那么他們也必須按照順序執(zhí)行 synchronized 塊中的代碼,第一個進入代碼塊的線程會創(chuàng)建一個新的Singleton實例,而后續(xù)的線程則因為無法通過if判斷,而不會創(chuàng)建多余的實例。
上述描述似乎已經(jīng)解決了我們面臨的所有問題,但實際上,從 JVM 的角度講,這些代碼仍然可能發(fā)生錯誤。
對于 JVM 而言,它執(zhí)行的是一個個 Java 指令。在 Java 指令中創(chuàng)建對象和賦值操作是分開進行的,也就是說instance = new Singleton();語句是分兩步執(zhí)行的。但是 JVM 并不保證這兩個操作的先后順序,也就是說有可能 JVM 會為新的 Singleton 實例分配空間,然后直接賦值給 instance 成員,然后再去初始化這個 Singleton 實例。這樣就使出錯成為了可能,我們?nèi)匀灰訟、B兩個線程為例:
- A、B線程同時進入了第一個if判斷
- A首先進入synchronized塊,由于instance為null,所以它執(zhí)行instance = new Singleton();
- 由于JVM內(nèi)部的優(yōu)化機制,JVM先畫出了一些分配給Singleton實例的空白內(nèi)存,并賦值給instance成員(注意此時JVM沒有開始初始化這個實例),然后A離開了synchronized塊。
- B進入synchronized塊,由于instance此時不是null,因此它馬上離開了synchronized塊并將結果返回給調(diào)用該方法的程序。
- 此時B線程打算使用Singleton實例,卻發(fā)現(xiàn)它沒有被初始化,于是錯誤發(fā)生了。
4. 通過內(nèi)部類實現(xiàn)多線程環(huán)境中的單例模式
為了實現(xiàn)慢加載,并且不希望每次調(diào)用 getInstance 時都必須互斥執(zhí)行,最好并且最方便的解決辦法如下:
public class Singleton{ private Singleton(){ … } private static class SingletonContainer{ private static Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingletonContainer.instance; } }JVM內(nèi)部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。
這樣當我們第一次調(diào)用getInstance的時候,JVM能夠幫我們保證instance只被創(chuàng)建一次,并且會保證把賦值給instance的內(nèi)存初始化完畢,這樣我們就不用擔心3.2中的問題。此外該方法也只會在第一次調(diào)用的時候使用互斥機制,這樣就解決了3.1中的低效問題。
最后 instance 是在第一次加載 SingletonContainer 類時被創(chuàng)建的,而 SingletonContainer 類則在調(diào)用 getInstance 方法的時候才會被加載,因此也實現(xiàn)了惰性加載。
總結
以上是生活随笔為你收集整理的Java 单例模式:懒加载(延迟加载)和即时加载的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 香帅的北大金融学课笔记2 -- 银行
- 下一篇: 香帅的北大金融学课笔记10 -- 金融衍