Java 單例模式:懶加載(延遲加載)和即時加載


引言

在開發中,如果某個實例的創建需要消耗很多系統資源,那么我們通常會使用惰性加載機制(或懶加載、延時加載),也就是說只有當使用到這個實例的時候才會創建這個實例,這個好處在單例模式中得到了廣泛應用。這個機制在單線程環境下的實現非常簡單,然而在多線程環境下卻存在隱患。

1、單例模式的惰性加載

通常當我們設計一個單例類的時候,會在類的內部構造這個類(通過構造函數,或者在定義處直接創建),並對外提供一個static getInstance() 方法提供獲取該單例對象的途徑。

public class Singleton        
{        
    private static Singleton instance = new Singleton();        
    private Singleton(){        
        …        
    }        
    public static Singleton getInstance(){        
          return instance;         
    }        
} 

這樣的代碼缺點是:第一次加載類的時候會連帶着創建 Singleton 實例,這樣的結果與我們所期望的不同,因為創建實例的時候可能並不是我們需要這個實例的時候。同時如果這個Singleton 實例的創建非常消耗系統資源,而應用始終都沒有使用 Singleton 實例,那么創建 Singleton 消耗的系統資源就被白白浪費了。

為了避免這種情況,我們通常使用惰性加載的機制,也就是在使用的時候才去創建。

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 同時執行了該方法,然后以如下方式執行:

  • A 進入 if 判斷,此時 foo 為 null,因此進入 if 內
  • B 進入 if 判斷,此時 A 還沒有創建 foo,因此 foo 也為 null,因此 B 也進入 if 內
  • A 創建了一個 Foo 並返回
  • B 也創建了一個 Foo 並返回

此時問題出現了,我們的單例被創建了兩次,而這並不是我們所期望的。

3. 各種解決方案及其存在的問題

3.1 使用 Class 鎖機制

以上問題最直觀的解決辦法就是給 getInstance 方法加上一個 synchronize 前綴,這樣每次只允許一個現成調用 getInstance 方法:

public static synchronized Singleton getInstance(){        
    if (instance == null)        
    instance = new Singleton();         
    return instance;         
}       

這種解決辦法的確可以防止錯誤的出現,但是它卻很影響性能:每次調用 getInstance 方法的時候都必須獲得 Singleton 的鎖,而實際上,當單例實例被創建以后,其后的請求沒有必要再使用互斥機制了

3.2 double-checked locking

曾經有人為了解決以上問題,提出了 double-checked locking 的解決方案

public static Singleton getInstance(){        
    if (instance == null)        
        synchronized(instance){        
            if(instance == null)        
                instance = new Singleton();        
        }        
    return instance;         
} 

讓我們來看一下這個代碼是如何工作的:首先當一個線程發出請求后,會先檢查 instance 是否為null,如果不是則直接返回其內容,這樣避免了進入 synchronized 塊所需要花費的資源。其次,即使第2節提到的情況發生了,兩個線程同時進入了第一個 if 判斷,那么他們也必須按照順序執行 synchronized 塊中的代碼,第一個進入代碼塊的線程會創建一個新的Singleton實例,而后續的線程則因為無法通過if判斷,而不會創建多余的實例。

上述描述似乎已經解決了我們面臨的所有問題,但實際上,從 JVM 的角度講,這些代碼仍然可能發生錯誤。

對於 JVM 而言,它執行的是一個個 Java 指令。在 Java 指令中創建對象和賦值操作是分開進行的,也就是說instance = new Singleton();語句是分兩步執行的。但是 JVM 並不保證這兩個操作的先后順序,也就是說有可能 JVM 會為新的 Singleton 實例分配空間,然后直接賦值給 instance 成員,然后再去初始化這個 Singleton 實例。這樣就使出錯成為了可能,我們仍然以A、B兩個線程為例:

  • A、B線程同時進入了第一個if判斷
  • A首先進入synchronized塊,由於instance為null,所以它執行instance = new Singleton();
  • 由於JVM內部的優化機制,JVM先畫出了一些分配給Singleton實例的空白內存,並賦值給instance成員(注意此時JVM沒有開始初始化這個實例),然后A離開了synchronized塊。
  • B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給調用該方法的程序。
  • 此時B線程打算使用Singleton實例,卻發現它沒有被初始化,於是錯誤發生了。

4. 通過內部類實現多線程環境中的單例模式

為了實現慢加載,並且不希望每次調用 getInstance 時都必須互斥執行,最好並且最方便的解決辦法如下:

public class Singleton{        
    private Singleton(){        
        …        
    }        
    private static class SingletonContainer{        
        private static Singleton instance = new Singleton();        
    }        
    public static Singleton getInstance(){        
        return SingletonContainer.instance;        
    }        
}   

JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。

這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance只被創建一次,並且會保證把賦值給instance的內存初始化完畢,這樣我們就不用擔心3.2中的問題。此外該方法也只會在第一次調用的時候使用互斥機制,這樣就解決了3.1中的低效問題。

最后 instance 是在第一次加載 SingletonContainer 類時被創建的,而 SingletonContainer 類則在調用 getInstance 方法的時候才會被加載,因此也實現了惰性加載。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM