1. 程式人生 > >Java 懶漢式單例 餓漢式單例

Java 懶漢式單例 餓漢式單例

轉載請註明出處:http://blog.csdn.net/mr_liabill/article/details/48374921   來自《LiaBin的部落格》

單例模式很常見,在面試中也會經常直接讓你寫一個單例出來

單例模式寫法一般分為兩種,懶漢式和餓漢式

餓漢式

public class SingleTon {
    //載入類的時候會初始化static的instance,從這以後,這個static的instance物件便一直佔著這段記憶體,永遠不會被回收掉。
    private static SingleTon instance = new SingleTon();

    //將建構函式private掉,避免直接new SingleTon()
    private SingleTon() {
    }

    /**
     * 因為是單例,所以只能通過static方法來獲取例項,因此必須是static的。
     * 方法實現較為簡單,因為instance已經在載入類的時候被初始化好了,所以不存在多執行緒併發造成的問題
     */
    public static SingleTon getInstance() {
        return instance;
    }
}

優點:

不需要考慮多執行緒問題,因為instance是靜態的,在類載入的時候就已經例項化了,同時也避免了synchronized所造成的效能問題,

缺點:

但這種方式也有點弊端,因為初始化類的時候就需要構造例項,(即便你還沒有用到這個例項),因此在某些特定條件下會耗費記憶體。

懶漢式

方式1: 基於volatile的雙重檢查鎖定的解決方案

為什麼需要按如下這麼複雜的去實現,參考有詳細的解釋 http://blog.csdn.net/guolin_blog/article/details/8860649

public class SingleTon {
    private static SingleTon instance = null;

    //將建構函式private掉,避免直接new SingleTon()
    private SingleTon() {
    }

    //synchronized避免多執行緒帶來的問題,但同時效率降低,另一方面採取多重鎖定,提高效率
    public static SingleTon getInstance() {
        if (null == instance) {
            synchronized (SingleTon.class) {
                if (null == instance) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}
這樣寫是沒問題的,因為私有建構函式中沒有初始化任何屬性,否則的話上面的程式碼還需要改進
public class SingleTon {
    private static volatile SingleTon instance = null;
    private String name;

    //將建構函式private掉,避免直接new SingleTon()
    private SingleTon() {
        name = "SingleTon";
    }

    //synchronized避免多執行緒帶來的問題,但同時效率降低,另一方面採取多重鎖定,提高效率
    public static SingleTon getInstance() {
        if (null == instance) {
            synchronized (SingleTon.class) {
                if (null == instance) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}
這裡為什麼需要使用volatile關鍵字呢?

假設執行緒thread1走到了第15行的if判斷髮現instance==null成立,於是都進入了外部的if體。這時候thread1先獲取了synchronized塊的鎖,於是thread1執行緒會執行第18行的instance = new SingleTon();這句程式碼,問題就出在這裡,這條語句它不是原子性執行的。在Java裡,例項化一個物件的過程簡單地講,可以分為兩步1)先為instance物件分配一塊記憶體,2)在這塊記憶體裡為instance物件裡的成員變數賦值(比如第11行裡為url賦值)。假設當thread1執行完第1)步而還沒有執行第2)步的時候,另外一個執行緒thread2走到了第15行,這時候instance已經不是null了,於是thread2直接返回了這個instance物件。有什麼問題呢?instance物件的初始化(變數賦值等操作)還沒執行完呢!thread2裡直接得到了一個沒有初始化完全的物件,就有可能導致很嚴重的問題了。

那麼volatile關鍵字有啥作用呢?當用volatile修飾了instance變數之後,對instance的寫操作”先行發生“於對它的讀操作。(這是Java虛擬機器裡的先行發生原則)這樣就保證了,thread1中的instance變數被完全初始化之後,thread2才能讀取它,當沒有完成初始化時,thread2只能等會兒啦。

優點:

需要的時候才去載入,記憶體消耗好一些。(因為在需要的時候才載入,所以叫懶漢式)

缺點:

程式碼如此複雜,,還有synchronized帶來的執行效率問題,呼叫同步方法會慢不少

方式2: 基於類初始化的解決方案

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

內部類的初始化是延遲的,外部類初始化時不會初始化內部類,只有在使用的時候才會初始化內部類。而Java語言規範規定,對於每一個類或介面C,都有一個唯一的初始化鎖LC與之對應。也就是說,SingletonHolder在各個執行緒初始化的時候是同步執行的,且全權由JVM承包了。

兩種延遲初始化方案總結

延遲初始化降低了初始化類或建立例項的開銷,但增加了訪問被延遲初始化的欄位的開銷。在大多數時候,正常的初始化要優於延遲初始化。如果確實需要對例項欄位使用執行緒安全的延遲初始化,請使用上面介紹的基於volatile的延遲初始化的方案;如果確實需要對靜態欄位使用執行緒安全的延遲初始化,請使用上面介紹的基於類初始化的方案。


總結

為了省麻煩,就用惡漢式吧。但是我一般用懶漢式,比較需要的時候才載入,可以節省記憶體。至於synchronized引起的效率問題,基本很少有這樣的場景,因為很少有兩個執行緒併發呼叫getInstance方法。