延遲載入單例模式(IoDH)引發的NoClassDefFoundError
一、問題背景
最近題主釋出在公司的SDK遇到了一個Bug。有關單例模式的,什麼問題呢?
我們先回想下,單例模式怎麼寫。30分鐘學透設計模式1-單例模式的前世今生
簡而言之:
- 私有的構造方法
- 提供一個靜態可以獲取例項物件的方法
其分類可大致分為:
- 非延遲載入(餓漢)
- 延遲載入(懶漢等)
問題:
題主使用的是:initialization-on-demand holder idiom 這種方法實現。然而卻丟擲了以下異常:
java.lang.NoClassDefFoundError: Could not initialize class SingleTon$Holder
at SingleTon.getInstance (SingleTon.java:11)
at SingleTon.main(SingleTon.java:24)
看下大致實現單例程式碼:
public class SingleTon {
private SingleTon() {}
private static class Holder {
private static final SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance() {
return Holder.INSTANCE;
}
}
乍一看,SingleTon這個類都已經執行到了,怎麼會找不到其內部類Holder
呢?NoClassDefFoundError
常見的場景是,類不存在或版本衝突。這裡都不滿足,我們復現下這個問題。
二、問題復現
如果在構造SingleTon
物件時,丟擲異常會怎樣?
public class SingleTon {
private SingleTon() {
int i = 1 / 0;
}
private static class Holder {
private static final SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance() {
return Holder.INSTANCE;
}
public static void main(String[] args) {
try {
System.out.println("First");
SingleTon.getInstance();
} catch (Throwable t) {
t.printStackTrace();
}
try {
System.out.println("Second");
SingleTon.getInstance();
} catch (Throwable t) {
t.printStackTrace();
}
}
}
看一下輸出:
First
Second
java.lang.ExceptionInInitializerError
at SingleTon.getInstance(SingleTon.java:11)
at SingleTon.main(SingleTon.java:17)
Caused by: java.lang.ArithmeticException: / by zero
at SingleTon.<init>(SingleTon.java:3)
at SingleTon.<init>(SingleTon.java:1)
at SingleTon$Holder.<clinit>(SingleTon.java:7)
... 2 more
java.lang.NoClassDefFoundError: Could not initialize class SingleTon$Holder
at SingleTon.getInstance(SingleTon.java:11)
at SingleTon.main(SingleTon.java:24)
初步分析是:IoDH這種單例實現為執行緒安全的延遲載入方式。
第一次呼叫getInstance()
時,由於出現了異常導致SingleTon
物件沒有生成,進而導致該Holder類沒有成功載入。
第二次呼叫時,則會出現NoClassDefFoundError
。
三、問題分析
IoDH(initialization on demand holder) 為一種延遲載入且執行緒安全的單例模式實現方式。這種方式的實現依賴於JVM對類載入過程中初始化階段的執行。
分析下這個單例類的初始化過程:
- 當SingleTon
類被JVM載入時,由於這個類沒有其他靜態屬性,其初始化過程會順利完成。但是內部靜態類Holder
直到呼叫getInstance()
時才會被初始化。
- 當Holder
第一次被執行時,JVM會載入並初始化該類。由於Holder
含有靜態方法INSTANCE
,因此會一併初始化INSTANCE
。JLS保證了類的初始化階段是連續的。這樣,所有後序的併發呼叫getInstance()
都會返回一個正確初始化的INSTANCE
而不會有額外同步開銷。
- 但是,任何初始化失敗都會導致單例類不可用。也就是說,IoDH這種實現方式只能用於能保證初始化不會失敗的情況。