一将谊、 單例模式
1. 什么是單例模式
單例模式(Singleton Pattern)是 Java 中最簡(jiǎn)單的設(shè)計(jì)模式之一财异。單例表示某一個(gè)類(lèi)只有一個(gè)實(shí)例 姻灶,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例红碑。這種類(lèi)型的設(shè)計(jì)模式屬于創(chuàng)建型模式名船,它提供了一種創(chuàng)建對(duì)象的最佳方式绰上。這種模式涉及到一個(gè)單一的類(lèi),該類(lèi)負(fù)責(zé)創(chuàng)建自己的對(duì)象渠驼,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建蜈块。這個(gè)類(lèi)提供了一種訪(fǎng)問(wèn)其唯一的對(duì)象的方式,可以直接訪(fǎng)問(wèn)迷扇,不需要實(shí)例化該類(lèi)的對(duì)象百揭。
注意:
1、單例類(lèi)只能有一個(gè)實(shí)例蜓席。
2信峻、單例類(lèi)必須自己創(chuàng)建自己的唯一實(shí)例。
3瓮床、單例類(lèi)必須給所有其他對(duì)象提供這一實(shí)例。
2. 單例模式的使用場(chǎng)景
確保每個(gè)類(lèi)有且只有一個(gè)對(duì)象的場(chǎng)景,避免產(chǎn)生多個(gè)對(duì)象消耗過(guò)多的資源隘庄,或者某種類(lèi)型的對(duì)象應(yīng)該有且只有一個(gè)踢步。例如,創(chuàng)建的一個(gè)對(duì)象需要消耗的資源過(guò)多丑掺,比如 I/O 與數(shù)據(jù)庫(kù)的連接等获印。
意圖:保證一個(gè)類(lèi)僅有一個(gè)實(shí)例,并提供一個(gè)訪(fǎng)問(wèn)它的全局訪(fǎng)問(wèn)點(diǎn)街州。
何時(shí)使用:當(dāng)您想控制實(shí)例數(shù)目兼丰,節(jié)省系統(tǒng)資源的時(shí)候。
如何解決:判斷系統(tǒng)是否已經(jīng)有這個(gè)單例唆缴,如果有則返回鳍征,如果沒(méi)有則創(chuàng)建。
關(guān)鍵代碼:構(gòu)造函數(shù)是私有的面徽。
應(yīng)用實(shí)例: 一些設(shè)備管理器常常設(shè)計(jì)為單例模式艳丛,比如一個(gè)電腦有兩臺(tái)打印機(jī),在輸出的時(shí)候就要處理不能兩臺(tái)打印機(jī)打印同一個(gè)文件趟紊。
優(yōu)點(diǎn):
1氮双、 在內(nèi)存里只有一個(gè)實(shí)例,減少了內(nèi)存的開(kāi)銷(xiāo)霎匈,尤其是頻繁的創(chuàng)建和銷(xiāo)毀實(shí)例(比如管理學(xué)院首頁(yè)頁(yè)面緩存)
2戴差、 避免對(duì)資源的多重占用(比如寫(xiě)文件操作)。
缺點(diǎn):沒(méi)有接口铛嘱,不能繼承暖释,與單一職責(zé)原則沖突,一個(gè)類(lèi)應(yīng)該只關(guān)心內(nèi)部邏輯弄痹,而不關(guān)心外面怎么樣來(lái)實(shí)例化饭入。
注意事項(xiàng):getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線(xiàn)程同時(shí)進(jìn)入造成 instance 被多次實(shí)例化。
3. 單例模式的實(shí)現(xiàn)
單例模式的UML圖如下
實(shí)現(xiàn)單例模式的關(guān)鍵點(diǎn):
- 構(gòu)造函數(shù)不對(duì)外開(kāi)放肛真,一般為private谐丢;
- 通過(guò)一個(gè)靜態(tài)方法或者枚舉返回單例類(lèi)對(duì)象;
- 確保單例類(lèi)的對(duì)象有且只有一個(gè)蚓让,尤其是在多線(xiàn)程的環(huán)境下乾忱;
- 確保單例類(lèi)對(duì)象在反序列化時(shí)不會(huì)重新構(gòu)建對(duì)象。
4. 單例模式的幾種實(shí)現(xiàn)方式
單例模式的實(shí)現(xiàn)有多種方式历极,如下所示:
1窄瘟、懶漢式,線(xiàn)程不安全
- 是否 Lazy 初始化:是
- 是否多線(xiàn)程安全:否
- 實(shí)現(xiàn)難度:易
描述:這種方式是最基本的實(shí)現(xiàn)方式趟卸,這種實(shí)現(xiàn)最大的問(wèn)題就是不支持多線(xiàn)程蹄葱。因?yàn)闆](méi)有加鎖 synchronized氏义,所以嚴(yán)格意義上它并不算單例模式。
這種方式 lazy loading 很明顯图云,不要求線(xiàn)程安全惯悠,在多線(xiàn)程不能正常工作。
實(shí)例
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
接下來(lái)介紹的幾種實(shí)現(xiàn)方式都支持多線(xiàn)程竣况,但是在性能上有所差異克婶。
2、懶漢式丹泉,線(xiàn)程安全
- 是否 Lazy 初始化:是
- 是否多線(xiàn)程安全:是
- 實(shí)現(xiàn)難度:易
描述:這種方式具備很好的 lazy loading情萤,能夠在多線(xiàn)程中很好的工作,但是摹恨,效率很低筋岛,99% 情況下不需要同步。
優(yōu)點(diǎn):第一次調(diào)用才初始化睬塌,避免內(nèi)存浪費(fèi)泉蝌。
缺點(diǎn):必須加鎖 synchronized 才能保證單例,但加鎖會(huì)影響效率揩晴。getInstance() 的性能對(duì)應(yīng)用程序不是很關(guān)鍵(該方法使用不太頻繁)勋陪。
實(shí)例
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3、餓漢式
- 是否 Lazy 初始化:否
- 是否多線(xiàn)程安全:是
- 實(shí)現(xiàn)難度:易
描述:這種方式比較常用硫兰,但容易產(chǎn)生垃圾對(duì)象诅愚。
優(yōu)點(diǎn):沒(méi)有加鎖,執(zhí)行效率會(huì)提高劫映。
缺點(diǎn):類(lèi)加載時(shí)就初始化违孝,浪費(fèi)內(nèi)存。
它基于 classloader 機(jī)制避免了多線(xiàn)程的同步問(wèn)題泳赋,不過(guò)雌桑,instance 在類(lèi)裝載時(shí)就實(shí)例化,雖然導(dǎo)致類(lèi)裝載的原因有很多種祖今,在單例模式中大多數(shù)都是調(diào)用 getInstance 方法校坑, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類(lèi)裝載,這時(shí)候初始化 instance 顯然沒(méi)有達(dá)到 lazy loading 的效果千诬。
實(shí)例
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
4耍目、雙檢鎖/雙重校驗(yàn)鎖(DCL,即 double-checked locking)
JDK 版本:JDK1.5 起
- 是否 Lazy 初始化:是
- 是否多線(xiàn)程安全:是
- 實(shí)現(xiàn)難度:較復(fù)雜
描述:這種方式采用雙鎖機(jī)制徐绑,安全且在多線(xiàn)程情況下能保持高性能邪驮。getInstance() 的性能對(duì)應(yīng)用程序很關(guān)鍵。
優(yōu)點(diǎn):資源利用率高傲茄,第一次執(zhí)行g(shù)etInstance時(shí)單例對(duì)象才會(huì)被實(shí)例化毅访,效率高沮榜。
缺點(diǎn):第一次加載時(shí)反應(yīng)稍慢,也由于Java的內(nèi)存模型原因?qū)е屡紶柺 ?/p>
實(shí)例
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
5喻粹、登記式/靜態(tài)內(nèi)部類(lèi)
- 是否 Lazy 初始化:是
- 是否多線(xiàn)程安全:是
- 實(shí)現(xiàn)難度:一般
描述:這種方式能達(dá)到雙檢鎖方式一樣的功效敞映,但實(shí)現(xiàn)更簡(jiǎn)單。對(duì)靜態(tài)域使用延遲初始化磷斧,應(yīng)使用這種方式而不是雙檢鎖方式。這種方式只適用于靜態(tài)域的情況捷犹,雙檢鎖方式可在實(shí)例域需要延遲初始化時(shí)使用弛饭。
這種方式同樣利用了 classloader 機(jī)制來(lái)保證初始化 instance 時(shí)只有一個(gè)線(xiàn)程,它跟第 3 種方式不同的是:第 3 種方式只要 Singleton 類(lèi)被裝載了萍歉,那么 instance 就會(huì)被實(shí)例化(沒(méi)有達(dá)到 lazy loading 效果)侣颂,而這種方式是 Singleton 類(lèi)被裝載了,instance 不一定被初始化枪孩。因?yàn)?SingletonHolder 類(lèi)沒(méi)有被主動(dòng)使用憔晒,只有通過(guò)顯式調(diào)用 getInstance 方法時(shí),才會(huì)顯式裝載 SingletonHolder 類(lèi)蔑舞,從而實(shí)例化 instance拒担。想象一下,如果實(shí)例化 instance 很消耗資源攻询,所以想讓它延遲加載从撼,另外一方面,又不希望在 Singleton 類(lèi)加載時(shí)就實(shí)例化钧栖,因?yàn)椴荒艽_保 Singleton 類(lèi)還可能在其他的地方被主動(dòng)使用從而被加載低零,那么這個(gè)時(shí)候?qū)嵗?instance 顯然是不合適的。這個(gè)時(shí)候拯杠,這種方式相比第 3 種方式就顯得很合理掏婶。
實(shí)例
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
6、枚舉
JDK 版本:JDK1.5 起
- 是否 Lazy 初始化:否
- 是否多線(xiàn)程安全:是
- 實(shí)現(xiàn)難度:易
-
描述:*這種實(shí)現(xiàn)方式還沒(méi)有被廣泛采用潭陪,但這是實(shí)現(xiàn)單例模式的最佳方法雄妥。它更簡(jiǎn)潔,自動(dòng)支持序列化機(jī)制畔咧,絕對(duì)防止多次實(shí)例化茎芭。
這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線(xiàn)程同步問(wèn)題誓沸,而且還自動(dòng)支持序列化機(jī)制梅桩,防止反序列化重新創(chuàng)建新的對(duì)象,絕對(duì)防止多次實(shí)例化拜隧。不過(guò)宿百,由于 JDK1.5 之后才加入 enum 特性趁仙,用這種方式寫(xiě)不免讓人感覺(jué)生疏,在實(shí)際工作中垦页,也很少用雀费。
不能通過(guò) reflection attack 來(lái)調(diào)用私有構(gòu)造方法。
實(shí)例
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
總結(jié):一般情況下痊焊,不建議使用第 1 種和第 2 種懶漢方式盏袄,建議使用第 3 種餓漢方式。只有在要明確實(shí)現(xiàn) lazy loading 效果時(shí)薄啥,才會(huì)使用第 5 種登記方式辕羽。如果涉及到反序列化創(chuàng)建對(duì)象時(shí),可以嘗試使用第 6 種枚舉方式垄惧。如果有其他特殊的需求刁愿,可以考慮使用第 4 種雙檢鎖方式。
二到逊、 工廠(chǎng)模式
1. 簡(jiǎn)介
工廠(chǎng)模式(Factory Pattern)是 Java 中最常用的設(shè)計(jì)模式之一铣口。這種類(lèi)型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式觉壶。在工廠(chǎng)模式中脑题,我們?cè)趧?chuàng)建對(duì)象時(shí)不會(huì)對(duì)客戶(hù)端暴露創(chuàng)建邏輯,并且是通過(guò)使用一個(gè)共同的接口來(lái)指向新創(chuàng)建的對(duì)象掰曾。
意圖:定義一個(gè)創(chuàng)建對(duì)象的接口旭蠕,讓其子類(lèi)自己決定實(shí)例化哪一個(gè)工廠(chǎng)類(lèi),工廠(chǎng)模式使其創(chuàng)建過(guò)程延遲到子類(lèi)進(jìn)行旷坦。
主要解決:主要解決接口選擇的問(wèn)題掏熬。
何時(shí)使用:我們明確地計(jì)劃不同條件下創(chuàng)建不同實(shí)例時(shí)。
如何解決:讓其子類(lèi)實(shí)現(xiàn)工廠(chǎng)接口秒梅,返回的也是一個(gè)抽象的產(chǎn)品旗芬。
關(guān)鍵代碼:創(chuàng)建過(guò)程在其子類(lèi)執(zhí)行。
應(yīng)用實(shí)例: 需要一輛汽車(chē)捆蜀,可以直接從工廠(chǎng)里面提貨疮丛,而不用去管這輛汽車(chē)是怎么做出來(lái)的,以及這個(gè)汽車(chē)?yán)锩娴木唧w實(shí)現(xiàn)辆它。
優(yōu)點(diǎn):
1誊薄、一個(gè)調(diào)用者想創(chuàng)建一個(gè)對(duì)象,只要知道其名稱(chēng)就可以了锰茉。
2呢蔫、擴(kuò)展性高,如果想增加一個(gè)產(chǎn)品飒筑,只要擴(kuò)展一個(gè)工廠(chǎng)類(lèi)就可以片吊。
3绽昏、屏蔽產(chǎn)品的具體實(shí)現(xiàn),調(diào)用者只關(guān)心產(chǎn)品的接口俏脊。
缺點(diǎn):每次增加一個(gè)產(chǎn)品時(shí)全谤,都需要增加一個(gè)具體類(lèi)和對(duì)象實(shí)現(xiàn)工廠(chǎng),使得系統(tǒng)中類(lèi)的個(gè)數(shù)成倍增加爷贫,在一定程度上增加了系統(tǒng)的復(fù)雜度认然,同時(shí)也增加了系統(tǒng)具體類(lèi)的依賴(lài)。這并不是什么好事漫萄。
使用場(chǎng)景:
1季眷、日志記錄器:記錄可能記錄到本地硬盤(pán)、系統(tǒng)事件卷胯、遠(yuǎn)程服務(wù)器等,用戶(hù)可以選擇記錄日志到什么地方威酒。
2窑睁、數(shù)據(jù)庫(kù)訪(fǎng)問(wèn),當(dāng)用戶(hù)不知道最后系統(tǒng)采用哪一類(lèi)數(shù)據(jù)庫(kù)葵孤,以及數(shù)據(jù)庫(kù)可能有變化時(shí)担钮。
3、設(shè)計(jì)一個(gè)連接服務(wù)器的框架尤仍,需要三個(gè)協(xié)議箫津,"POP3"、"IMAP"宰啦、"HTTP"苏遥,可以把這三個(gè)作為產(chǎn)品類(lèi),共同實(shí)現(xiàn)一個(gè)接口赡模。
注意事項(xiàng):作為一種創(chuàng)建類(lèi)模式田炭,在任何需要生成復(fù)雜對(duì)象的地方,都可以使用工廠(chǎng)方法模式漓柑。有一點(diǎn)需要注意的地方就是復(fù)雜對(duì)象適合使用工廠(chǎng)模式教硫,而簡(jiǎn)單對(duì)象,特別是只需要通過(guò) new 就可以完成創(chuàng)建的對(duì)象辆布,無(wú)需使用工廠(chǎng)模式瞬矩。如果使用工廠(chǎng)模式,就需要引入一個(gè)工廠(chǎng)類(lèi)锋玲,會(huì)增加系統(tǒng)的復(fù)雜度景用。
2. 實(shí)現(xiàn)
我們將創(chuàng)建一個(gè) Shape 接口和實(shí)現(xiàn) Shape 接口的實(shí)體類(lèi)。下一步是定義工廠(chǎng)類(lèi) ShapeFactory嫩絮。
FactoryPatternDemo丛肢,我們的演示類(lèi)使用 ShapeFactory 來(lái)獲取 Shape 對(duì)象围肥。它將向 ShapeFactory 傳遞信息(CIRCLE / RECTANGLE / SQUARE),以便獲取它所需對(duì)象的類(lèi)型蜂怎。
3. 實(shí)現(xiàn)步驟
步驟 1
創(chuàng)建一個(gè)接口:
Shape.java
public interface Shape {
void draw();
}
步驟 2
創(chuàng)建實(shí)現(xiàn)接口的實(shí)體類(lèi)穆刻。
Rectangle.java
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
Square.java
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
Circle.java
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
步驟 3
創(chuàng)建一個(gè)工廠(chǎng),生成基于給定信息的實(shí)體類(lèi)的對(duì)象杠步。
ShapeFactory.java
public class ShapeFactory {
//使用 getShape 方法獲取形狀類(lèi)型的對(duì)象
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
}
步驟 4
使用該工廠(chǎng)氢伟,通過(guò)傳遞類(lèi)型信息來(lái)獲取實(shí)體類(lèi)的對(duì)象。
FactoryPatternDemo.java
public class FactoryPatternDemo {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();
//獲取 Circle 的對(duì)象幽歼,并調(diào)用它的 draw 方法
Shape shape1 = shapeFactory.getShape("CIRCLE");
//調(diào)用 Circle 的 draw 方法
shape1.draw();
//獲取 Rectangle 的對(duì)象朵锣,并調(diào)用它的 draw 方法
Shape shape2 = shapeFactory.getShape("RECTANGLE");
//調(diào)用 Rectangle 的 draw 方法
shape2.draw();
//獲取 Square 的對(duì)象,并調(diào)用它的 draw 方法
Shape shape3 = shapeFactory.getShape("SQUARE");
//調(diào)用 Square 的 draw 方法
shape3.draw();
}
}
步驟 5
執(zhí)行程序甸私,輸出結(jié)果:
Inside Circle::draw() method.
Inside Rectangle::draw() method.
Inside Square::draw() method.