1. 程式人生 > >變數的延遲初始化

變數的延遲初始化

在絕大多數的系統中,我們都會使用正常的初始化。正常的初始化程式碼是這樣的:

private final MyClass field = new MyCLass();

但在有些程式中,我們不希望某些變數在正常的類載入過程中就被初始化。換言之,我們希望某些變數能夠延遲初始化。在閱讀下面的內容之前,筆者希望你能讀一下我的另外一篇文章, 單例模式的5種JAVA實現。你要問我為什麼?答案只有四個字,“見多識廣”!本文要介紹的內容大多采用這篇部落格的技術方法。

說正題,如何實現延遲初始化呢?最開始想到的可能是醬紫:

    private MyClass field;//正確的程式碼
    
    public MyClass getField(){
        synchronized (this){
            if(field==null){
                field=new MyClass();
            }
        }
        return field;
    }

你肯定會問,怎麼變這麼複雜啦?下面這樣豈不是更好?

    private MyClass field;//錯誤的程式碼

    public MyClass getField() {
        if (field == null) {
            field = new MyClass();
        }
        return field;
    }

上面是錯誤的程式碼,為何?想象下想在有兩個執行緒併發呼叫getField()方法,存在這樣的一個時序,可能讓兩個執行緒都執行了一遍new MyClass() 這個時候,到底隨的才算是合法的初始化過程呢?很顯然不對。那麼正常的初始化不存在這個問題嗎?恭喜你,問對地方了。答案是正常的初始化過程不存在這樣的問題。因為正常的變數初始化過程是在類的<clinit>方法中進行的,而<clinit>方法是被JVM同步執行的。是不是又漲姿勢了。具體的原理可以參見我的部落格
JVM之型別的生命週期


那麼問題就很明瞭了,既然人家正常的初始化流程需要同步執行,那麼延遲的初始化流程也需要被同步執行。區別僅僅在於前者的同步工作是JVM來完成,而後者的同步工作需要程式設計師自己完成。誰讓你非要延遲初始化呢!


言歸正傳,通過上面介紹的正確的程式碼,我們就可以實現延遲初始化了。但是這樣的程式碼併發度太低,因此我們使用雙重校驗鎖的方案。如下程式碼:

    private volatile MyClass field;

    public MyClass getField() {
        if(field==null) {
            synchronized (this) {
                if (field == null) {
                    field = new MyClass();
                }
            }
        }
        return field;
    }
這是雙重校驗鎖模式的程式碼,為何這麼寫,還請讀者參照 單例模式的5種JAVA實現中的雙重校驗鎖模式,那裡面講的很明白,在此不贅述。


當然我們也可以使用靜態內部類的方案來進行變數的延遲初始化。程式碼如下:

    private static class Holder{
        private static final MyClass field=new MyClass();
    }

    public static MyClass getField() {
        return Holder.field;
    }

好啦,介紹了3種方法來實現變數的延遲初始化。那麼哪種方案好呢?


簡言之,大多數的變數應該使用正常的初始化流程,而不是延遲初始化。當必須使用延遲初始化時,就可以使用對應的延遲初始化方法。對於例項域,我們使用雙重校驗鎖來實現效能更高。對於靜態域,我們使用靜態內部類來實現效能更高。


筆者開設了一個知乎live,詳細的介紹的JAVA從入門到精通該如何學,學什麼?

提供給想深入學習和提高JAVA能力的同學,歡迎收聽https://www.zhihu.com/lives/932192204248682496