Java設計模式之單例模式

1、前期回顧

上一篇《Java設計模式之開篇》介紹了設計的六大原則,分別是,單一職責、里氏替換原則、依賴倒置、迪米特法則、接口隔離、開閉原則。每個原則都經過定義解釋和代碼實戰進行詳細體現,最後也總結了這六大原則,原則是死的,人是活的,咱們要根據實際狀況是使用六大原則,不要生搬硬套,爲了原則而原則,爲了模式而模式。這一篇,咱們來介紹下設計模式最簡單的一個模式,單例模式。java

2、釋義以及實戰

  • 2.1 單例模式的定義

單例模式,英文:Singleton Pattern,英文解釋:Ensure a class has only instance,and provide a global point of access to it.翻譯過來就是說,要確保一個類只有一個實例,並且自行實例化而且向整個系統提供這個實例。數據庫

  • 2.2 單例模式的使用場景

單例模式的使用場景要求能夠用一句話表示,若是一個類有多個對象會致使系統可能出現問題就要採用單例模式,通常的場景以下:編程

a.建立一個對象須要耗費大量資源或者時間,如IO,數據庫鏈接等。設計模式

b.生成惟一id狀況。安全

c.工具類,通常就能夠採用靜態類能夠知足。bash

  • 2.3 單例模式的實戰

咱們來設計一個單例模式,場景:中國古代,通常狀況,某一段時間只能有一個皇帝,固然特殊狀況除外,不管是大臣仍是平民,求見的皇帝都是同一個,咱們用代碼實現這個場景多線程

//皇帝接口
public interface Iemperor {
    //皇帝下命令
    public void sayCommand(String str);
}

//明朝皇帝實現類
public class MingEmperor implements Iemperor {
    private static  MingEmperor emperor=new MingEmperor(new Random().nextInt(10)+"");
    //皇帝身份id
    private String id;
    //防止破壞單例
    private MingEmperor(String id) {
        this.id = id;
    }

    @Override
    public void sayCommand(String str) {
        System.out.println(str+"----------我是皇帝,這是個人id="+id);
    }
    public static MingEmperor getEmperor(){
        return emperor;
    }
}

//場景客戶類
public class Client {
    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            MingEmperor.getEmperor().sayCommand("求見皇帝");
        }
    }
}

複製代碼

執行下場景類,輸出結果爲dom

這說明,咱們的單例模式成功了,這裏咱們經過聲明一個全局靜態變量,在類的初始化階段就實例化一個對象,而後每次獲取都是同一個對象。這種方式被稱之爲:餓漢氏單例模式。該單例模式的缺點就是要在初始化時候實例化對象,若是這種模式對象太多,就會建立大量的對象,並且有些可能還用不到。因此咱們就改造下這種模式,變爲懶漢氏單例模式,只有在真正須要用到對象的時候纔開始實例化對象。咱們改造下皇帝實現類代碼:

public class MingEmperor implements Iemperor {
    private static  MingEmperor emperor;
    //皇帝身份id
    private String id;
    //防止破壞單例
    private MingEmperor(String id) {
        this.id = id;
    }

    @Override
    public void sayCommand(String str) {
        System.out.println(str+"----------我是皇帝,這是個人id="+id);
    }
    public static MingEmperor getEmperor(){
        if (emperor==null){
            emperor= new MingEmperor(new Random().nextInt(10)+"");
        }
        return emperor;
    }
}
複製代碼

這裏獲取皇帝對象的時候,判斷是否爲空,若是爲空就new一個對象,不然直接返回以前實例化過的對象。咱們按照原來的場景類運行下,結果以下:ide

這和咱們預期結果同樣,難道這樣就ok了嗎?既然是單例的,那麼多線程環境下確定也是單例的,咱們換成多線程試試。工具

public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    MingEmperor.getEmperor().sayCommand("求見皇帝");
                }
            }).start();
        }
    }
複製代碼

執行結果:

出問題了,多線程環境下皇帝都不是同一個了,這在古代是要出大問題啊。那麼爲何會出現這樣的狀況呢?由於在多線程環境下,能夠理解爲是並行去獲取皇帝對象,那麼第一個線程獲取的時候,發現皇帝對象爲空,那麼就去new一個對象,第二個線程也有可能獲取爲空,那麼本身也去new一個皇帝對象,因此就會出現上圖這樣的狀況。那麼咱們如何改造來保證線程安全呢?有人說給獲取實例的方法加上synchronized鎖,沒錯,這樣是能夠解決問題,可是效率過低了,那麼有沒有什麼更高效的方法呢?答案是,有,咱們改造下代碼:

public class MingEmperor implements Iemperor {
    //增長volatile修飾,防止虛擬機指令重排序
    private static volatile   MingEmperor emperor;
    //皇帝身份id
    private String id;
    //防止破壞單例
    private MingEmperor(String id) {
        this.id = id;
    }


    @Override
    public void sayCommand(String str) {
        System.out.println(str+"----------我是皇帝,這是個人id="+id);
    }
    public static synchronized MingEmperor getEmperor(){
        if (emperor==null){
        //採用同步代碼塊,縮小鎖定範圍,比直接同步方法效率要略高
            synchronized (MingEmperor.class){
            //這裏再判斷爲空,是防止別的線程已經完成了實例化,這裏重複實例化了,就違反了單例。
                if (emperor==null) emperor= new MingEmperor(new Random().nextInt(10)+"");
            }
        }
        return emperor;
    }
}
複製代碼

以上代碼改造,主增長了volatile修飾全局變量,該變量主要功能就是增長線程之間的可見性,同時防止指令重排序(關於volatile變量,後續我會出一片文章詳細說)。另外縮小了synchronized的範圍,採用同步代碼塊。這樣就完成了線程安全的懶漢式單例模式,該寫法被稱爲,雙重檢查鎖定(DCL)。

其實咱們結合上一篇的知識,再看看這部分代碼,發現這個單例模式違反了一個原則,就是單一職責原則,按照單一職責原則,皇帝類不用關心什麼單例不單例,我只要傳達命令就行了。因此這個模式也告訴了你們,原則要靈活使用。

  • 2.4 單例模式的缺點

1.剛剛上面提到的,單例模式違反了單一職責。

2.單例模式嚴格意義上說是沒有接口的,要擴展只能修改,雖然上面的例子實現了接口,可是並不能針對接口作一個單例模式,由於單例模式要求「自行實例化」,接口和抽象類是不能被實例化的。因此在每一個實現類進行單例模式,就算實現了接口,每一個實現類都要本身實現一套單例的邏輯,也就是形成了代碼重複。

  • 2.5 單例模式的優勢

單例模式主要優勢就算減小了系統資源消耗,優化了系統性能。

3、總結

其實,咱們用到的池化技術能夠理解爲單例模式的一種擴展,池化技術就是能夠容許建立指定數量的實例,而單例模式就至關於池化數量爲1 。因此,模式在於要消化理解,而後靈活變通使用。另外須要注意的是,咱們在設計單例模式的時候還須要考慮到一種破壞單例模式的狀況,就是克隆方式,雖然咱們私有化了構造方法,可是克隆對象並不須要執行構造方法,因此這裏也是一個潛在破壞單例模式的方式。解決方法就是單例類不要實現Cloneable接口。

4、參考

《設計模式之禪》

6、推薦閱讀

JAVA設計模式之開篇

帶你走進java集合之ArrayList

帶你走進java集合之HashMap

Java鎖之ReentrantLock(一)

Java鎖之ReentrantLock(二)

Java鎖之ReentrantReadWriteLock

JAVA NIO編程入門(一)

JAVA NIO 編程入門(二)

JAVA NIO 編程入門(三)

相關文章
相關標籤/搜索