前情提要
上集講到, 小光建立了開分店的標準(工廠), 以後開分店都按照這套標準執行(從CompanyFactory的實現中生產開分店的必須東西), 開分店變得更加容易了.
小光也是馬上將自己的這套”開分公司的工廠”投入使用了, 開出了花山軟體新城分店.
隨著分店越來越多, 小光也請了分別請了店長來”代理”小光之前的職責. 當然, 小光可不能完全放任不管啊, 他想著我至少得知道下每天各個店的基本情況吧.
所有示例原始碼已經上傳到Github, 戳這裡
日報制度
小光想到了當時做程式猿時, 敏捷開發每天站立會議的三個問題:
- 昨天完成了什麼
- 今天要做什麼
- 有什麼困難, 阻力
心想, 我也可以根據這個弄個日報制度啊, 讓各個店長按照這個格式彙報下當天的戰績:
表格:
public class Form {
private ArrayList<String> mFormData = new ArrayList<>();
public void write(String data) {
mFormData.add(data);
}
@Override
public String toString() {
return "表格:" + this.hashCode() + ", 資料:" + mFormData;
}
}複製程式碼
每天讓各個店子在打烊之前在系統中拿當日的表格(如果還沒有, 就建立一個)來填寫資料, 然後提交.
出了問題
想法挺好, 但是剛剛用上, 就出了問題:
光谷店店長表妹登入系統, 發現2016-12-16這個資料夾中還沒有表格檔案, 於是本地建立了一個, 用來填寫資料, 準備稍後提交. 然而此時, 花山店的店長小章也登入了系統, 也發現還沒有表格檔案, 也建立了一個…
讓我們來看下操作:
表妹:
public class Cousins {
public Form submitReport() {
Form form = new Form();
form.write("光谷店資料");
return form;
}
}複製程式碼
小章:
public class XiaoZhang {
public Form submitReport() {
Form form = new Form();
form.write("花山店資料");
return form;
}
}複製程式碼
兩人的使用流程:
public class Demo {
public static void main(String[] args) {
Cousins cousins = new Cousins();
Form form = cousins.submitReport();
System.out.println(form);
XiaoZhang xiaoZhang = new XiaoZhang();
Form form2 = xiaoZhang.submitReport();
System.out.println(form2);
}
}複製程式碼
來看下結果:
表格:1639705018, 資料:[光谷店資料]
表格:1627674070, 資料:[花山店資料]複製程式碼
最終這個資料夾中有了兩個(不同的)表格, 小光看起來很是不方便…
表格應該只能有一份
表格只能有一份, 小光心想. 那麼怎麼保證呢, 很簡單, 我提前給建立好, 大家通過統一的介面來取這個檔案, 而不能自己建立. 這樣就不會有問題了:
public class HungryForm extends Form {
// 提前建立好
private static HungryForm sInstance = new HungryForm();
// 私有化的構造, 避免別人直接建立表格
private HungryForm() {}
// 店長們通過這個介面來取表格
public static HungryForm getInstance() {
return sInstance;
}
}複製程式碼
店長們這樣提交報告:
public class Cousins {
public Form submitReport() {
// 直接新建一個表格
// Form form = new Form();
// 從固定的介面取表格
Form form = HungryForm.getInstance();
form.write("光谷店資料");
return form;
}
}
public class XiaoZhang {
public Form submitReport() {
// 直接新建一個表格
// Form form = new Form();
// 從固定的介面取表格
Form form = HungryForm.getInstance();
form.write("花山店資料");
return form;
}
}複製程式碼
提交方式不變:
public class Demo {
public static void main(String[] args) {
Cousins cousins = new Cousins();
Form form = cousins.submitReport();
System.out.println(form);
XiaoZhang xiaoZhang = new XiaoZhang();
Form form2 = xiaoZhang.submitReport();
System.out.println(form2);
}
}複製程式碼
來看下現在的結果:
表格:1639705018, 資料:[光谷店資料]
表格:1639705018, 資料:[光谷店資料, 花山店資料]複製程式碼
可以看到兩人用的是同一份表格(hashCode一樣的), 生成的資料也沒有問題了.
故事之後
看到這, 同學們應該都看出來了, 小光這就是使用了大名鼎鼎的單例模式.
照例, 看下類圖, 這個應該是最簡單的類圖了:
單例模式
保證一個類(HungryForm)僅有一個例項(sInstance), 並提供一個訪問該例項的全域性訪問點(getInstance).
這就意味著單例通常有如下兩個特點:
- 建構函式是私有的(避免別的地方建立它)
- 有一個static的方法來對外提供一個該單例的例項.
擴充套件閱讀一
同學們可能注意到了, 我們在這個單例模式中使用了Hungry這個詞, 沒錯, 我們這裡實現單例的方式使用的就是餓漢式.
1, 餓漢式單例
餓漢式單例
顧名思義, 就是很餓, 不管三七二十一先建立了一個例項放著, 而不管最終用不用.
然而, 這個單例可能最終並不需要, 如果提前就建立好, 就會浪費記憶體空間了.
例如, 我們這個故事中, 年底假期中, 所有店子都歇業十天, 這十天就沒有任何店長會去訪問這個表格, 然而小光還是都每天都建立了, 這就造成了空間浪費(假設這個表格資料(物件例項)很大…)
2, 懶漢式單例
那麼怎麼辦呢?
我們可以使用懶漢式單例:
public class LazyForm extends Form {
private static LazyForm sInstance;
// 私有化的構造, 避免別人直接建立表格
private LazyForm() {}
// 店長們通過這個介面來取表格
public static LazyForm getInstance() {
// 在有店長訪問該檔案時才建立, 通過判斷當前檔案是否存在(sInstance == null)來避免重複建立
if (sInstance == null) {
sInstance = new LazyForm();
}
return sInstance;
}
}複製程式碼
懶漢式單例
“懶”, 也就是現在懶得建立, 等有使用者要用的時候才建立.
3, 執行緒安全的懶漢式單例
但是這樣建立也會有問題啊, 因為他是通過sInstance == null判斷當前是否已經存在表格檔案的, 假設有兩個店長同時呼叫getInstance來取檔案, 同時走到sInstance == null判斷這一步, 就會出問題了 — 有可能建立了兩個檔案(例項), 就達不到單例的目的了.
所以說這種懶漢式是執行緒不安全的, 在多執行緒環境下, 並不能做到單例.
那麼, 該如何做, 既能懶載入, 又執行緒安全呢?
我們都知道Java中多執行緒環境往往會用到synchronized關鍵字, 通過他來做執行緒併發性控制.
synchronized方法控制對類成員變數的訪問, 每個類例項對應一把鎖, synchronized修飾的方法必須獲得呼叫該方法的類例項的鎖方能執行, 否則所屬執行緒阻塞. 方法一旦執行, 就獨佔該鎖. 直到從該方法返回時才將鎖釋放. 此後被阻塞的執行緒方能獲得該鎖, 重新進入可執行狀態.
讓我們來看下執行緒安全的懶漢式單例:
public class SynchronizedLazyForm extends Form {
private static SynchronizedLazyForm sInstance;
// 私有化的構造, 避免別人直接建立表格
private SynchronizedLazyForm() {}
// 店長們通過這個介面來取表格
// 注意, 這是一個synchronized方法
// 參考https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html
public static synchronized SynchronizedLazyForm getInstance() {
// 在有店長訪問該檔案時才建立, 通過判斷當前檔案是否存在(sInstance == null)來避免重複建立
if (sInstance == null) {
sInstance = new SynchronizedLazyForm();
}
return sInstance;
}
}複製程式碼
執行緒安全的懶漢式單例
利用synchronized關鍵字來修飾對外提供該類唯一例項的介面(getInstance)來確保在一個執行緒呼叫該介面時能阻塞(block)另一個執行緒的呼叫, 從而達到多執行緒安全, 避免重複建立單例.
然而, synchronized有很大的效能開銷. 而且在這裡我們是修飾了getInstance方法, 意味著, 如果getInstance被很多執行緒頻繁呼叫時, 每次都會做同步檢查, 會導致程式效能下降.
實際上我們要的是單例, 當單例已經存在的時候, 我們是不需要用同步方法來控制的. 一如我們第一種單例的實現—餓漢模式單例, 我們一開始就建立好了單例, 就無需擔心執行緒同步問題.
但是餓漢模式是提前建立, 那麼我們怎麼能做到延遲建立, 且執行緒安全, 且效能有所提升呢?
4, 雙重檢查鎖定DCL(Double-Checked Locking)單例
如上所言, 我們想要的是單例, 故而單例已經存在的情況下我們無需做同步檢查, 如下實現:
public class DCLForm extends Form {
// 注意, 這裡我們引入了volatile關鍵字
private volatile static DCLForm sInstance;
// 私有化的構造, 避免別人直接建立表格
private DCLForm() {}
// 店長們通過這個介面來取表格
public static DCLForm getInstance() {
// 第一次檢查
if (sInstance == null) {
// 第一次呼叫getInstance時, sInstance為空, 進入此分支
// 使用synchronized block來確保多執行緒的安全
synchronized (DCLForm.class) {
// 第二次檢查
if (sInstance == null) {
sInstance = new DCLForm();
}
}
}
return sInstance;
}
}複製程式碼
- 捨棄了同步方法
- 在getInstance時, 先檢查單例是否已經存在, 如果存在了, 我們無需同步操作了, 任何執行緒過來直接取單例就行, 大大提升了效能.
- 若單例不存在(第一次呼叫時), 使用synchronized同步程式碼塊, 來確保進入的只有一個執行緒, 在此再做一次單例存在與否的檢查, 進而建立出單例.
這樣就保證了:
- 在單例還沒有建立時, 多個執行緒同時呼叫getInsance時, 保證只有一個執行緒能夠執行sInstance = new DCLForm()建立單例.
- 在單例已經存在時, getInsance沒有加鎖, 直接訪問, 訪問建立好的單例, 從而達到效能提升.
注意
這裡我們對sInstance使用的volatile關鍵字
具體原因和原理, 請參考這篇文章, 講的很詳細.
然而, 使用volatile關鍵字的雙重檢查方案需要JDK5及以上(因為從JDK5開始使用新的JSR-133記憶體模型規範,這個規範增強了volatile的語義).
那麼我們還有什麼更通用的方式能保證多執行緒單例建立, 以及懶載入方式呢?
5, 靜態內部類單例
public class StaticInnerClassForm extends Form {
// 私有化的構造, 避免別人直接建立表格
private StaticInnerClassForm() {}
// 店長們通過這個介面來取表格
public static StaticInnerClassForm getInstance() {
return FormHolder.INSTANCE;
}
// 在靜態內部類中例項化該單例
private static class FormHolder {
private static final StaticInnerClassForm INSTANCE = new StaticInnerClassForm();
}
}複製程式碼
這種方式, 通過JVM的類載入方式(虛擬機器會保證一個類的初始化在多執行緒環境中被正確的加鎖、同步), 來保證了多執行緒併發訪問的正確性.
另外, 由於靜態內部類的載入特性 — 在使用時才載入, 這種方式也達成了懶載入的目的.
顯然, 這種方式是一種比較完美的單例模式. 當然, 它也有其弊端, 依賴特定程式語言, 適用於JAVA平臺.
還有很多單例的實現模式, 例如利用JDK 5起的Enum 列舉單例模式, 使用容器類管理的單例模式等, 在此就不一一說了, 網上都比較氾濫了…
從使用上, 如果是單執行緒環境的, 個人推薦使用第二種懶漢式單例, 簡單便捷. 如果考慮多執行緒同步的話, 推薦使用第五種靜態內部類單例, 確保同步且懶載入完美結合.
好了, 小光建立並改善了一套完整的日報系統. 這樣, 他每天就可以看到各個分店的戰況了, 也能根據各個店的問題, 來及時協調資源解決, 保證各個分店的良好運轉了.