網上結論:
我們先來看看網上普遍的結論:
所謂“懶漢式”與“餓漢式”的區別,是在與建立單例物件的時間的不同。
“懶漢式”是在你真正用到的時候才去建這個單例物件
“餓漢式是在類建立的同時就已經建立好一個靜態的物件,不管你用的用不上,一開始就建立這個單例物件
先不說結論,看看下文
程式碼實現:
餓漢式
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("懶漢式單例初始化!");
}
public static 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 enum SingletonEnum {
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一個靜態的單例物件。
- 當需要防止反射和序列化破壞單例的時候,推薦用列舉類的單例模式