Java設計模式之單例模式

單例模式,是特別常見的一種設計模式,所以咱們有必要對它的概念和幾種常見的寫法很是瞭解,並且這也是面試中常問的知識點。程序員

所謂單例模式,就是全部的請求都用一個對象來處理,如咱們經常使用的Spring默認就是單例的,而多例模式是每一次請求都建立一個新的對象來處理,如structs2中的action。面試

使用單例模式,能夠確保一個類只有一個實例,而且易於外部訪問,還能夠節省系統資源。若是在系統中,但願某個類的對象只存在一個,就可使用單例模式。設計模式

那怎麼確保一個類只有一個實例呢?安全

咱們知道,一般咱們會經過new關鍵字來建立一個新的對象。這個時候類的構造函數是public公有的,你能夠隨意建立多個類的實例。因此,首先咱們須要把構造函數改成private私有的,這樣就不能隨意new對象了,也就控制了多個實例的隨意建立。多線程

而後,定義一個私有的靜態屬性,來表明類的實例,它只能類內部訪問,不容許外部直接訪問。jvm

最後,經過一個靜態的公有方法,把這個私有靜態屬性返回出去,這就爲系統建立了一個全局惟一的訪問點。ide

以上,就是單例模式的三個要素。總結爲:函數

  1. 私有構造方法
  2. 指向本身實例的私有靜態變量
  3. 對外的靜態公共訪問方法

單例模式分爲餓漢式和懶漢式。它們的主要區別就是,實例化對象的時機不一樣。餓漢式,是在類加載時就會實例化一個對象。懶漢式,則是在真正使用的時候纔會實例化對象。測試

餓漢式單例代碼實現:優化

public class Singleton {

    // 餓漢式單例,直接建立一個私有的靜態實例
    private static Singleton singleton = new Singleton();

    //私有構造方法
    private Singleton(){

    }

    //提供一個對外的靜態公有方法
    public static Singleton getInstance(){
        return singleton;

    }
}

懶漢式單例代碼實現

public class Singleton {

    // 懶漢式單例,類加載時先不建立實例
    private static Singleton singleton = null;

    //私有構造方法
    private Singleton(){

    }

    //真正使用時才建立類的實例
    public static Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

稍有經驗的程序員就發現了,以上懶漢式單例的實現方式,在單線程下是沒有問題的。可是,若是在多線程中使用,就會發現它們返回的實例有可能不是同一個。咱們能夠經過代碼來驗證一下。建立十個線程,分別啓動,線程內去得到類的實例,把實例的 hashcode 打印出來,只要相同則認爲是同一個實例;若不一樣,則說明建立了多個實例。

public class TestSingleton {
    public static void main(String[] args) {
        for (int i = 0; i < 10 ; i++) {
            new MyThread().start();
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton.hashCode());
    }
}
/**
運行屢次,就會發現,hashcode會出現不一樣值
668770925
668770925
649030577
668770925
668770925
668770925
668770925
668770925
668770925
668770925
*/

因此,以上懶漢式的實現方式是線程不安全的。那餓漢式呢?你能夠手動測試一下,會發現無論運行多少次,返回的hashcode都是相同的。所以,認爲餓漢式單例是線程安全的。

那爲何餓漢式就是線程安全的呢?這是由於,餓漢式單例在類加載時,就建立了類的實例,也就是說在線程去訪問單例對象以前就已經建立好實例了。而一個類在整個生命週期中只會被加載一次。所以,也就能夠保證明例只有一個。因此說,餓漢式單例天生就是線程安全的。(能夠了解一下類加載機制)

既然懶漢式單例不是線程安全的,那麼咱們就須要去改造一下,讓它在多線程環境下也能正常工做。如下介紹幾種常見的寫法:

1) 使用synchronized方法

實現很是簡單,只須要在方法上加一個synchronized關鍵字便可

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){

    }

    //使用synchronized修飾方法,便可保證線程安全
    public static synchronized Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

這種方式,雖然能夠保證線程安全,可是同步方法的做用域太大,鎖的粒度比較粗,所以,執行效率就比較低。

2) synchronized 同步塊

既然,同步整個方法的做用域大,那我縮小範圍,在方法裏邊,只同步建立實例的那一小部分代碼塊不就能夠了嗎(由於方法較簡單,因此鎖代碼塊和鎖方法沒什麼明顯區別)。

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //synchronized只修飾方法內部的部分代碼塊
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

這種方法,本質上和第一種沒什麼區別,所以,效率提高不大,能夠忽略不計。

3) 雙重檢測(double check)

能夠看到,以上的第二種方法只要調用getInstance方法,就會走到同步代碼塊裏。所以,會對效率產生影響。其實,咱們徹底能夠先判斷實例是否已經存在。若已經存在,則說明已經建立好實例了,也就不須要走同步代碼塊了;若不存在即爲空,才進入同步代碼塊,這樣能夠提升執行效率。所以,就有如下雙重檢測了:

public class Singleton {

    //注意,此變量須要用volatile修飾以防止指令重排序
    private static volatile Singleton singleton = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //進入方法內,先判斷實例是否爲空,以肯定是否須要進入同步代碼塊
        if(singleton == null){
            synchronized (Singleton.class){
                //進入同步代碼塊時也須要判斷實例是否爲空
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

須要注意的一點是,此方式中,靜態實例變量須要用volatile修飾。由於,new Singleton() 是一個非原子性操做,其流程爲:

a.給 singleton 實例分配內存空間
b.調用Singleton類的構造函數建立實例
c.將 singleton 實例指向分配的內存空間,這時認爲singleton實例不爲空

正常順序爲 a->b->c,可是,jvm爲了優化編譯程序,有時候會進行指令重排序。就會出現執行順序爲 a->c->b。這在多線程中就會表現爲,線程1執行了new對象操做,而後發生了指令重排序,會致使singleton實例已經指向了分配的內存空間(c),可是實際上,實例還沒建立完成呢(b)。

這個時候,線程2就會認爲實例不爲空,判斷 if(singleton == null)爲false,因而不走同步代碼塊,直接返回singleton實例(此時拿到的是未實例化的對象),所以,就會致使線程2的對象不可用而使用時報錯。

4)使用靜態內部類

思考一下,因爲類加載是按需加載,而且只加載一次,因此能保證線程安全,這也是爲何說餓漢式單例是天生線程安全的。一樣的道理,咱們是否是也能夠經過定義一個靜態內部類來保證類屬性只被加載一次呢。

public class Singleton {

    private Singleton(){

    }

    //靜態內部類
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance(){
        //調用內部類的屬性,獲取單例對象
        return Holder.singleton;
    }
}

並且,JVM在加載外部類的時候,不會加載靜態內部類,只有在內部類的方法或屬性(此處即指singleton實例)被調用時纔會加載,所以不會形成空間的浪費。

5)使用枚舉類

由於枚舉類是線程安全的,而且只會加載一次,因此利用這個特性,能夠經過枚舉類來實現單例。

public class Singleton {

    private Singleton(){

    }

    //定義一個枚舉類
    private enum SingletonEnum {
        //建立一個枚舉實例
        INSTANCE;

        private Singleton singleton;

        //在枚舉類的構造方法內實例化單例類
        SingletonEnum(){
            singleton = new Singleton();
        }

        private Singleton getInstance(){
            return singleton;
        }
    }

    public static Singleton getInstance(){
        //獲取singleton實例
        return SingletonEnum.INSTANCE.getInstance();
    }
}
相關文章
相關標籤/搜索