1. 程式人生 > >延遲載入單例模式(IoDH)引發的NoClassDefFoundError

延遲載入單例模式(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這種實現方式只能用於能保證初始化不會失敗的情況。