一、前期回顧
上一篇《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("求見皇帝");
}
}
}
複製程式碼
執行下場景類,輸出結果為
這說明,我們的單例模式成功了,這裡我們通過宣告一個全域性靜態變數,在類的初始化階段就例項化一個物件,然後每次獲取都是同一個物件。這種方式被稱之為:餓漢氏單例模式。該單例模式的缺點就是要在初始化時候例項化物件,如果這種模式物件太多,就會建立大量的物件,而且有些可能還用不到。所以我們就改造下這種模式,變為懶漢氏單例模式,只有在真正需要用到物件的時候才開始例項化物件。我們改造下皇帝實現類程式碼: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一個物件,否則直接返回之前例項化過的物件。我們按照原來的場景類執行下,結果如下:
這和我們預期結果一樣,難道這樣就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 單例模式的優點
單例模式主要優點就算減少了系統資源消耗,優化了系統效能。
三、總結
其實,我們用到的池化技術可以理解為單例模式的一種擴充套件,池化技術就是可以允許建立指定數量的例項,而單例模式就相當於池化數量為1 。所以,模式在於要消化理解,然後靈活變通使用。另外需要注意的是,我們在設計單例模式的時候還需要考慮到一種破壞單例模式的情況,就是克隆方式,雖然我們私有化了構造方法,但是克隆物件並不需要執行構造方法,所以這裡也是一個潛在破壞單例模式的方式。解決方法就是單例類不要實現Cloneable介面。
四、參考
《設計模式之禪》