Java設計模式之單例模式

張瘋子啊發表於2020-12-02

Java設計模式之單例模式

一、前期回顧

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

二、釋義以及實戰

  • 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.工具類,一般就可以採用靜態類可以滿足。

  • 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("求見皇帝");
        }
    }
}

複製程式碼

執行下場景類,輸出結果為

Java設計模式之單例模式
這說明,我們的單例模式成功了,這裡我們通過宣告一個全域性靜態變數,在類的初始化階段就例項化一個物件,然後每次獲取都是同一個物件。這種方式被稱之為:餓漢氏單例模式。該單例模式的缺點就是要在初始化時候例項化物件,如果這種模式物件太多,就會建立大量的物件,而且有些可能還用不到。所以我們就改造下這種模式,變為懶漢氏單例模式,只有在真正需要用到物件的時候才開始例項化物件。我們改造下皇帝實現類程式碼:

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一個物件,否則直接返回之前例項化過的物件。我們按照原來的場景類執行下,結果如下:

Java設計模式之單例模式

這和我們預期結果一樣,難道這樣就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();
        }
    }
複製程式碼

執行結果:

Java設計模式之單例模式
出問題了,多執行緒環境下皇帝都不是同一個了,這在古代是要出大問題啊。那麼為什麼會出現這樣的情況呢?因為在多執行緒環境下,可以理解為是並行去獲取皇帝物件,那麼第一個執行緒獲取的時候,發現皇帝物件為空,那麼就去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 單例模式的優點

單例模式主要優點就算減少了系統資源消耗,優化了系統效能。

三、總結

其實,我們用到的池化技術可以理解為單例模式的一種擴充套件,池化技術就是可以允許建立指定數量的例項,而單例模式就相當於池化數量為1 。所以,模式在於要消化理解,然後靈活變通使用。另外需要注意的是,我們在設計單例模式的時候還需要考慮到一種破壞單例模式的情況,就是克隆方式,雖然我們私有化了構造方法,但是克隆物件並不需要執行構造方法,所以這裡也是一個潛在破壞單例模式的方式。解決方法就是單例類不要實現Cloneable介面。

四、參考

《設計模式之禪》

六、推薦閱讀

JAVA設計模式之開篇

帶你走進java集合之ArrayList

帶你走進java集合之HashMap

Java鎖之ReentrantLock(一)

Java鎖之ReentrantLock(二)

Java鎖之ReentrantReadWriteLock

JAVA NIO程式設計入門(一)

JAVA NIO 程式設計入門(二)

JAVA NIO 程式設計入門(三)

相關文章