別再用懶漢模式了——從JVM的角度看單例模式
我們先來看看網上普遍的結論:
所謂“懶漢式”與“餓漢式”的區別,是在與建立單例物件的時間的不同。
“懶漢式”是在你真正用到的時候才去建這個單例物件
“餓漢式是在類建立的同時就已經建立好一個靜態的物件,不管你用的用不上,一開始就建立這個單例物件
先不說結論,看看下文
程式碼實現:
餓漢式
public class Singleton1 { private final static Singleton1 singleton = new Singleton(); private Singleton1() { System.out.println("餓漢式單例初始化!"); } public static Singleton1 getSingleton () { return singleton; } } 複製程式碼
在類靜態變數裡直接new一個單例
懶漢式
public class Singleton2 { private volatile static Singleton2 singleton; // 5 private Singleton2() { System.out.println("懶漢式單例初始化!"); } publicstatic Singleton2 getInstance () { if(singleton ==null) {// 1 synchronized(Singleton2.class) {// 2 if(singleton == null) {// 3 singleton =new Singleton2(); //4 } } } return singleton; } } 複製程式碼
程式碼1 處的判空是為了減少同步方法的使用,提高效率
程式碼2,3 處的加鎖和判空是為了防止多執行緒下重複例項化單例。
程式碼5 處的volatile是為了防止多執行緒下程式碼4 的指令重排序
測試方法
建立一個Test測試類
public class Test { public static void main(String[] args) throws IOException { // 懶漢式 Singleton1 singleton1 = Singleton1.getInstance(); // 餓漢式 Singleton2 singleton2 = Singleton2.getInstance(); } } 複製程式碼
執行結果

從結果上看沒啥毛病,那我們來加個斷點試試。按照以往的認知,餓漢單例是在類載入的時候的例項化,那麼執行main方法應該會輸出餓漢單例的初始化,我們來看看結果:
public static void main(String[] args) throws IOException { System.in.read(); // 餓漢式 Singleton1 singleton1 = Singleton1.getInstance(); // 懶漢式 Singleton2 singleton2 = Singleton2.getInstance(); } 複製程式碼
此時執行結果:

如圖是沒有結果的,餓漢單例怎麼沒有例項化呢?原來餓漢單例是在本類載入的時候才例項化的,在斷點的時候還沒有載入餓漢單例。 我們來詳細複習一下類載入:
類的載入分為5個步驟: 載入、驗證、準備、解析、初始化
初始化就是執行編譯後的< cinit>()方法,而< cinit>()方法就是在編譯時將靜態變數賦值和靜態塊合併到一起生成的。
所以說,“餓漢模式”的建立物件是在類載入的初始化階段進行的,那麼類載入的初始化階段在什麼時候進行呢?jvm規範規定有且只有以下7種情況下會進行類載入的初始化階段:
- 使用new關鍵字例項化物件的時候
- 設定或讀取一個類的靜態欄位(被final修飾,已在編譯器把結果放入常量池的靜態欄位除外)的時候
- 呼叫一個類的靜態方法的時候
- 使用java.lang.reflect包的方法對類進行反射呼叫的時候
- 初始化一個類的子類(會首先初始化父類)
- 當虛擬機器啟動的時候,初始化包含main方法的主類
- 當使用jdk1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。
綜上, 基本來說就是隻有當你以某種方式呼叫了這個類的時候,它才會進行初始化,而不是jvm啟動的時候就初始化,而jvm本身會確保類的初始化只執行一次 。那如果不使用這個單例物件的話,記憶體中根本沒有Singleton例項物件,也就是和“懶漢模式”是 一樣的效果 。
當然,也有一種可能就是單例類裡除了getInstance()方法還有一些其他靜態方法,這樣當呼叫其他靜態方法的時候,也會初始化例項,但是這個很容易解決,只要加個 內部類 就行了:
public class Singleton { private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance () { return LazyHolder.INSTANCE; } } 複製程式碼
總結
網上的結論普遍說單例過早佔用資源,而推薦使用“懶漢模式”,但他們忽略了單例何時進行類載入,經過以上分析,“懶漢模式”實現複雜而且沒有任何獨佔優點, “餓漢模式”完勝 。“餓漢模式”使用場景推薦:
- 當單例類裡有其他靜態方法的時候,推薦使用靜態內部類的形式。
- 當單例類裡只有getInstance()方法的時候,推薦直接new一個靜態的單例物件。
更新:
關於列舉類的:這裡做個測試:
public enumSingletonEnum { INSTANCE; public SingletonEnum getInstance() { return INSTANCE; } SingletonEnum() { System.out.println("列舉類單例例項化啦"); } public static void test() { System.out.println("測試呼叫列舉類的靜態方法"); } } 複製程式碼
測試類:
public static void main(String[] args) throws IOException { SingletonEnum.test(); System.in.read(); SingletonEnum singletonEnum=SingletonEnum.INSTANCE; } 複製程式碼
由此得出結論,列舉類的單例和普通的“餓漢模式”一樣,都是在類載入(呼叫靜態方法)的時候初始化。但是列舉類的另一個優點是能預防反射和序列化,因此再次得出結論
- 當單例類裡有其他靜態方法的時候,推薦使用靜態內部類的形式。
- 當單例類裡只有getInstance()方法的時候,推薦直接new一個靜態的單例物件。
- 當需要防止反射和序列化破壞單例的時候,推薦用列舉類的單例模式