上集講到, 小光創建了開分店的標準(工廠), 之後開分店都按照這套標準執行(從CompanyFactory的實現中生產開分店的必須東西), 開分店變得更加容易了.javascript
小光也是立刻將本身的這套"開分公司的工廠"投入使用了, 開出了花山軟件新城分店.html
隨着分店愈來愈多, 小光也請了分別請了店長來"代理"小光以前的職責. 固然, 小光可不能徹底聽任無論啊, 他想着我至少得知道下天天各個店的基本狀況吧.java
全部示例源碼已經上傳到Github, 戳這裏git
小光想到了當時作程序猿時, 敏捷開發天天站立會議的三個問題: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這個文件夾中尚未表格文件, 因而本地建立了一個, 用來填寫數據, 準備稍後提交. 然而此時, 花山店的店長小章也登陸了系統, 也發現尚未表格文件, 也建立了一個...oracle
讓咱們來看下操做:
表妹:
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這個詞, 沒錯, 咱們這裏實現單例的方式使用的就是餓漢式.
餓漢式單例
顧名思義, 就是很餓, 無論三七二十一先建立了一個實例放着, 而無論最終用不用.
然而, 這個單例可能最終並不須要, 若是提早就建立好, 就會浪費內存空間了.
例如, 咱們這個故事中, 年末假期中, 全部店子都歇業十天, 這十天就沒有任何店長會去訪問這個表格, 然而小光仍是都天天都建立了, 這就形成了空間浪費(假設這個表格數據(對象實例)很大...)
那麼怎麼辦呢?
咱們可使用懶漢式單例:
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;
}
}複製代碼
懶漢式單例
"懶", 也就是如今懶得建立, 等有用戶要用的時候才建立.
可是這樣建立也會有問題啊, 由於他是經過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被不少線程頻繁調用時, 每次都會作同步檢查, 會致使程序性能降低.
實際上咱們要的是單例, 當單例已經存在的時候, 咱們是不須要用同步方法來控制的. 一如咱們第一種單例的實現---餓漢模式單例, 咱們一開始就建立好了單例, 就無需擔憂線程同步問題.
可是餓漢模式是提早建立, 那麼咱們怎麼能作到延遲建立, 且線程安全, 且性能有所提高呢?
如上所言, 咱們想要的是單例, 故而單例已經存在的狀況下咱們無需作同步檢查, 以下實現:
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;
}
}複製代碼
這樣就保證了:
注意
這裏咱們對sInstance使用的volatile關鍵字
具體緣由和原理, 請參考這篇文章, 講的很詳細.
然而, 使用volatile關鍵字的雙重檢查方案須要JDK5及以上(由於從JDK5開始使用新的JSR-133內存模型規範,這個規範加強了volatile的語義).
那麼咱們還有什麼更通用的方式能保證多線程單例建立, 以及懶加載方式呢?
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 枚舉單例模式, 使用容器類管理的單例模式等, 在此就不一一說了, 網上都比較氾濫了...
從使用上, 若是是單線程環境的, 我的推薦使用第二種懶漢式單例, 簡單便捷. 若是考慮多線程同步的話, 推薦使用第五種靜態內部類單例, 確保同步且懶加載完美結合.
好了, 小光建立並改善了一套完整的日報系統. 這樣, 他天天就能夠看到各個分店的戰況了, 也能根據各個店的問題, 來及時協調資源解決, 保證各個分店的良好運轉了.