
前情提要
上集講到, 小光建立了開(kāi)分店的標(biāo)準(zhǔn)(工廠), 以后開(kāi)分店都按照這套標(biāo)準(zhǔn)執(zhí)行(從CompanyFactory的實(shí)現(xiàn)中生產(chǎn)開(kāi)分店的必須東西), 開(kāi)分店變得更加容易了.
小光也是馬上將自己的這套"開(kāi)分公司的工廠"投入使用了, 開(kāi)出了花山軟件新城分店.
隨著分店越來(lái)越多, 小光也請(qǐng)了分別請(qǐng)了店長(zhǎng)來(lái)"代理"小光之前的職責(zé). 當(dāng)然, 小光可不能完全放任不管啊, 他想著我至少得知道下每天各個(gè)店的基本情況吧.
所有示例源碼已經(jīng)上傳到Github, 戳這里
日?qǐng)?bào)制度
小光想到了當(dāng)時(shí)做程序猿時(shí), 敏捷開(kāi)發(fā)每天站立會(huì)議的三個(gè)問(wèn)題:
- 昨天完成了什么
- 今天要做什么
- 有什么困難, 阻力
心想, 我也可以根據(jù)這個(gè)弄個(gè)日?qǐng)?bào)制度啊, 讓各個(gè)店長(zhǎng)按照這個(gè)格式匯報(bào)下當(dāng)天的戰(zhàn)績(jī):

表格:
public class Form {
private ArrayList<String> mFormData = new ArrayList<>();
public void write(String data) {
mFormData.add(data);
}
@Override
public String toString() {
return "表格:" + this.hashCode() + ", 數(shù)據(jù):" + mFormData;
}
}
每天讓各個(gè)店子在打烊之前在系統(tǒng)中拿當(dāng)日的表格(如果還沒(méi)有, 就創(chuàng)建一個(gè))來(lái)填寫數(shù)據(jù), 然后提交.
出了問(wèn)題
想法挺好, 但是剛剛用上, 就出了問(wèn)題:
光谷店店長(zhǎng)表妹登錄系統(tǒng), 發(fā)現(xiàn)2016-12-16這個(gè)文件夾中還沒(méi)有表格文件, 于是本地創(chuàng)建了一個(gè), 用來(lái)填寫數(shù)據(jù), 準(zhǔn)備稍后提交. 然而此時(shí), 花山店的店長(zhǎng)小章也登錄了系統(tǒng), 也發(fā)現(xiàn)還沒(méi)有表格文件, 也創(chuàng)建了一個(gè)...
讓我們來(lái)看下操作:
表妹:
public class Cousins {
public Form submitReport() {
Form form = new Form();
form.write("光谷店數(shù)據(jù)");
return form;
}
}
小章:
public class XiaoZhang {
public Form submitReport() {
Form form = new Form();
form.write("花山店數(shù)據(jù)");
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);
}
}
來(lái)看下結(jié)果:
表格:1639705018, 數(shù)據(jù):[光谷店數(shù)據(jù)]
表格:1627674070, 數(shù)據(jù):[花山店數(shù)據(jù)]
最終這個(gè)文件夾中有了兩個(gè)(不同的)表格, 小光看起來(lái)很是不方便...
表格應(yīng)該只能有一份
表格只能有一份, 小光心想. 那么怎么保證呢, 很簡(jiǎn)單, 我提前給創(chuàng)建好, 大家通過(guò)統(tǒng)一的接口來(lái)取這個(gè)文件, 而不能自己創(chuàng)建. 這樣就不會(huì)有問(wèn)題了:
public class HungryForm extends Form {
// 提前創(chuàng)建好
private static HungryForm sInstance = new HungryForm();
// 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
private HungryForm() {}
// 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
public static HungryForm getInstance() {
return sInstance;
}
}
店長(zhǎng)們這樣提交報(bào)告:
public class Cousins {
public Form submitReport() {
// 直接新建一個(gè)表格
// Form form = new Form();
// 從固定的接口取表格
Form form = HungryForm.getInstance();
form.write("光谷店數(shù)據(jù)");
return form;
}
}
public class XiaoZhang {
public Form submitReport() {
// 直接新建一個(gè)表格
// Form form = new Form();
// 從固定的接口取表格
Form form = HungryForm.getInstance();
form.write("花山店數(shù)據(jù)");
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);
}
}
來(lái)看下現(xiàn)在的結(jié)果:
表格:1639705018, 數(shù)據(jù):[光谷店數(shù)據(jù)]
表格:1639705018, 數(shù)據(jù):[光谷店數(shù)據(jù), 花山店數(shù)據(jù)]
可以看到兩人用的是同一份表格(hashCode一樣的), 生成的數(shù)據(jù)也沒(méi)有問(wèn)題了.
故事之后
看到這, 同學(xué)們應(yīng)該都看出來(lái)了, 小光這就是使用了大名鼎鼎的單例模式.
照例, 看下類圖, 這個(gè)應(yīng)該是最簡(jiǎn)單的類圖了:

單例模式
保證一個(gè)類(HungryForm)僅有一個(gè)實(shí)例(sInstance), 并提供一個(gè)訪問(wèn)該實(shí)例的全局訪問(wèn)點(diǎn)(getInstance).
這就意味著單例通常有如下兩個(gè)特點(diǎn):
- 構(gòu)造函數(shù)是私有的(避免別的地方創(chuàng)建它)
- 有一個(gè)static的方法來(lái)對(duì)外提供一個(gè)該單例的實(shí)例.
擴(kuò)展閱讀一
同學(xué)們可能注意到了, 我們?cè)谶@個(gè)單例模式中使用了Hungry這個(gè)詞, 沒(méi)錯(cuò), 我們這里實(shí)現(xiàn)單例的方式使用的就是餓漢式.
1, 餓漢式單例
餓漢式單例
顧名思義, 就是很餓, 不管三七二十一先創(chuàng)建了一個(gè)實(shí)例放著, 而不管最終用不用.
然而, 這個(gè)單例可能最終并不需要, 如果提前就創(chuàng)建好, 就會(huì)浪費(fèi)內(nèi)存空間了.
例如, 我們這個(gè)故事中, 年底假期中, 所有店子都歇業(yè)十天, 這十天就沒(méi)有任何店長(zhǎng)會(huì)去訪問(wèn)這個(gè)表格, 然而小光還是都每天都創(chuàng)建了, 這就造成了空間浪費(fèi)(假設(shè)這個(gè)表格數(shù)據(jù)(對(duì)象實(shí)例)很大...)
2, 懶漢式單例
那么怎么辦呢?
我們可以使用懶漢式單例:
public class LazyForm extends Form {
private static LazyForm sInstance;
// 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
private LazyForm() {}
// 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
public static LazyForm getInstance() {
// 在有店長(zhǎng)訪問(wèn)該文件時(shí)才創(chuàng)建, 通過(guò)判斷當(dāng)前文件是否存在(sInstance == null)來(lái)避免重復(fù)創(chuàng)建
if (sInstance == null) {
sInstance = new LazyForm();
}
return sInstance;
}
}
懶漢式單例
"懶", 也就是現(xiàn)在懶得創(chuàng)建, 等有用戶要用的時(shí)候才創(chuàng)建.
3, 線程安全的懶漢式單例
但是這樣創(chuàng)建也會(huì)有問(wèn)題啊, 因?yàn)樗峭ㄟ^(guò)sInstance == null判斷當(dāng)前是否已經(jīng)存在表格文件的, 假設(shè)有兩個(gè)店長(zhǎng)同時(shí)調(diào)用getInstance來(lái)取文件, 同時(shí)走到sInstance == null判斷這一步, 就會(huì)出問(wèn)題了 --- 有可能創(chuàng)建了兩個(gè)文件(實(shí)例), 就達(dá)不到單例的目的了.
所以說(shuō)這種懶漢式是線程不安全的, 在多線程環(huán)境下, 并不能做到單例.
那么, 該如何做, 既能懶加載, 又線程安全呢?
我們都知道Java中多線程環(huán)境往往會(huì)用到synchronized關(guān)鍵字, 通過(guò)他來(lái)做線程并發(fā)性控制.
synchronized方法控制對(duì)類成員變量的訪問(wèn), 每個(gè)類實(shí)例對(duì)應(yīng)一把鎖, synchronized修飾的方法必須獲得調(diào)用該方法的類實(shí)例的鎖方能執(zhí)行, 否則所屬線程阻塞. 方法一旦執(zhí)行, 就獨(dú)占該鎖. 直到從該方法返回時(shí)才將鎖釋放. 此后被阻塞的線程方能獲得該鎖, 重新進(jìn)入可執(zhí)行狀態(tài).
讓我們來(lái)看下線程安全的懶漢式單例:
public class SynchronizedLazyForm extends Form {
private static SynchronizedLazyForm sInstance;
// 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
private SynchronizedLazyForm() {}
// 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
// 注意, 這是一個(gè)synchronized方法
// 參考https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html
public static synchronized SynchronizedLazyForm getInstance() {
// 在有店長(zhǎng)訪問(wèn)該文件時(shí)才創(chuàng)建, 通過(guò)判斷當(dāng)前文件是否存在(sInstance == null)來(lái)避免重復(fù)創(chuàng)建
if (sInstance == null) {
sInstance = new SynchronizedLazyForm();
}
return sInstance;
}
}
線程安全的懶漢式單例
利用synchronized關(guān)鍵字來(lái)修飾對(duì)外提供該類唯一實(shí)例的接口(getInstance)來(lái)確保在一個(gè)線程調(diào)用該接口時(shí)能阻塞(block)另一個(gè)線程的調(diào)用, 從而達(dá)到多線程安全, 避免重復(fù)創(chuàng)建單例.
然而, synchronized有很大的性能開(kāi)銷. 而且在這里我們是修飾了getInstance方法, 意味著, 如果getInstance被很多線程頻繁調(diào)用時(shí), 每次都會(huì)做同步檢查, 會(huì)導(dǎo)致程序性能下降.
實(shí)際上我們要的是單例, 當(dāng)單例已經(jīng)存在的時(shí)候, 我們是不需要用同步方法來(lái)控制的. 一如我們第一種單例的實(shí)現(xiàn)---餓漢模式單例, 我們一開(kāi)始就創(chuàng)建好了單例, 就無(wú)需擔(dān)心線程同步問(wèn)題.
但是餓漢模式是提前創(chuàng)建, 那么我們?cè)趺茨茏龅窖舆t創(chuàng)建, 且線程安全, 且性能有所提升呢?
4, 雙重檢查鎖定DCL(Double-Checked Locking)單例
如上所言, 我們想要的是單例, 故而單例已經(jīng)存在的情況下我們無(wú)需做同步檢查, 如下實(shí)現(xiàn):
public class DCLForm extends Form {
// 注意, 這里我們引入了volatile關(guān)鍵字
private volatile static DCLForm sInstance;
// 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
private DCLForm() {}
// 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
public static DCLForm getInstance() {
// 第一次檢查
if (sInstance == null) {
// 第一次調(diào)用getInstance時(shí), sInstance為空, 進(jìn)入此分支
// 使用synchronized block來(lái)確保多線程的安全
synchronized (DCLForm.class) {
// 第二次檢查
if (sInstance == null) {
sInstance = new DCLForm();
}
}
}
return sInstance;
}
}
- 舍棄了同步方法
- 在getInstance時(shí), 先檢查單例是否已經(jīng)存在, 如果存在了, 我們無(wú)需同步操作了, 任何線程過(guò)來(lái)直接取單例就行, 大大提升了性能.
- 若單例不存在(第一次調(diào)用時(shí)), 使用synchronized同步代碼塊, 來(lái)確保進(jìn)入的只有一個(gè)線程, 在此再做一次單例存在與否的檢查, 進(jìn)而創(chuàng)建出單例.
這樣就保證了:
- 在單例還沒(méi)有創(chuàng)建時(shí), 多個(gè)線程同時(shí)調(diào)用getInsance時(shí), 保證只有一個(gè)線程能夠執(zhí)行sInstance = new DCLForm()創(chuàng)建單例.
- 在單例已經(jīng)存在時(shí), getInsance沒(méi)有加鎖, 直接訪問(wèn), 訪問(wèn)創(chuàng)建好的單例, 從而達(dá)到性能提升.
注意
這里我們對(duì)sInstance使用的volatile關(guān)鍵字
具體原因和原理, 請(qǐng)參考這篇文章, 講的很詳細(xì).
然而, 使用volatile關(guān)鍵字的雙重檢查方案需要JDK5及以上(因?yàn)閺腏DK5開(kāi)始使用新的JSR-133內(nèi)存模型規(guī)范,這個(gè)規(guī)范增強(qiáng)了volatile的語(yǔ)義).
那么我們還有什么更通用的方式能保證多線程單例創(chuàng)建, 以及懶加載方式呢?
5, 靜態(tài)內(nèi)部類單例
public class StaticInnerClassForm extends Form {
// 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
private StaticInnerClassForm() {}
// 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
public static StaticInnerClassForm getInstance() {
return FormHolder.INSTANCE;
}
// 在靜態(tài)內(nèi)部類中實(shí)例化該單例
private static class FormHolder {
private static final StaticInnerClassForm INSTANCE = new StaticInnerClassForm();
}
}
這種方式, 通過(guò)JVM的類加載方式(虛擬機(jī)會(huì)保證一個(gè)類的初始化在多線程環(huán)境中被正確的加鎖、同步), 來(lái)保證了多線程并發(fā)訪問(wèn)的正確性.
另外, 由于靜態(tài)內(nèi)部類的加載特性 --- 在使用時(shí)才加載, 這種方式也達(dá)成了懶加載的目的.
顯然, 這種方式是一種比較完美的單例模式. 當(dāng)然, 它也有其弊端, 依賴特定編程語(yǔ)言, 適用于JAVA平臺(tái).
還有很多單例的實(shí)現(xiàn)模式, 例如利用JDK 5起的Enum 枚舉單例模式, 使用容器類管理的單例模式等, 在此就不一一說(shuō)了, 網(wǎng)上都比較泛濫了...
從使用上, 如果是單線程環(huán)境的, 個(gè)人推薦使用第二種懶漢式單例, 簡(jiǎn)單便捷. 如果考慮多線程同步的話, 推薦使用第五種靜態(tài)內(nèi)部類單例, 確保同步且懶加載完美結(jié)合.
好了, 小光創(chuàng)建并改善了一套完整的日?qǐng)?bào)系統(tǒng). 這樣, 他每天就可以看到各個(gè)分店的戰(zhàn)況了, 也能根據(jù)各個(gè)店的問(wèn)題, 來(lái)及時(shí)協(xié)調(diào)資源解決, 保證各個(gè)分店的良好運(yùn)轉(zhuǎn)了.