創(chuàng)建型模式悴能,共五種:
- 單例 Singleton
- 原型 Prototype
- 抽象工廠 Abstract Factory
- 工廠方法 Factory Method
- 建造者 Builder
1 單例模式(Singleton)
描述
確保一個(gè)類只有一個(gè)實(shí)例,并提供該實(shí)例的全局訪問點(diǎn)速勇。
實(shí)現(xiàn)
實(shí)現(xiàn)要點(diǎn)
- 一個(gè)私有靜態(tài)變量
- 一個(gè)私有構(gòu)造函數(shù)
- 一個(gè)公有靜態(tài)函數(shù)
實(shí)現(xiàn)方式
Ⅰ 餓漢式-線程安全
線程不安全問題主要是由于 uniqueInstance 被實(shí)例化多次,采取直接實(shí)例化 uniqueInstance 的方式就不會(huì)產(chǎn)生線程不安全問題椿争。
但是直接實(shí)例化的方式也丟失了延遲實(shí)例化帶來的節(jié)約資源的好處亮曹。
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
}
public static Singleton getUniqueInstance() {
return uniqueInstance;
}
}
Ⅱ 懶漢式-線程不安全
以下實(shí)現(xiàn)中,私有靜態(tài)變量 uniqueInstance 被延遲實(shí)例化横浑,這樣做的好處是,如果沒有用到該類屉更,那么就不會(huì)實(shí)例化 uniqueInstance徙融,從而節(jié)約資源。
這個(gè)實(shí)現(xiàn)在多線程環(huán)境下是不安全的瑰谜,如果多個(gè)線程能夠同時(shí)進(jìn)入 if (uniqueInstance == null)
欺冀,并且此時(shí) uniqueInstance 為 null,那么會(huì)有多個(gè)線程執(zhí)行 uniqueInstance = new Singleton();
語句萨脑,這將導(dǎo)致實(shí)例化多次 uniqueInstance隐轩。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
Ⅲ 懶漢式-線程安全
只需要對 getUniqueInstance() 方法加鎖,那么在一個(gè)時(shí)間點(diǎn)只能有一個(gè)線程能夠進(jìn)入該方法渤早,從而避免了實(shí)例化多次 uniqueInstance职车。
但是當(dāng)一個(gè)線程進(jìn)入該方法之后,其它試圖進(jìn)入該方法的線程都必須等待鹊杖,即使 uniqueInstance 已經(jīng)被實(shí)例化了悴灵。這會(huì)讓線程阻塞時(shí)間過長,因此該方法有性能問題骂蓖,不推薦使用积瞒。
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
Ⅳ 雙重校驗(yàn)鎖-線程安全
uniqueInstance 只需要被實(shí)例化一次,之后就可以直接使用了登下。加鎖操作只需要對實(shí)例化那部分的代碼進(jìn)行茫孔,只有當(dāng) uniqueInstance 沒有被實(shí)例化時(shí),才需要進(jìn)行加鎖被芳。
雙重校驗(yàn)鎖先判斷 uniqueInstance 是否已經(jīng)被實(shí)例化缰贝,如果沒有被實(shí)例化,那么才對實(shí)例化語句進(jìn)行加鎖畔濒。
public class Singleton {
/**
* 雙重檢查鎖定的問題是:并不能保證它會(huì)在單處理器或多處理器計(jì)算機(jī)上順利運(yùn)行剩晴。
*
* 雙重檢查鎖定失敗的問題并不歸咎于 JVM 中的實(shí)現(xiàn) bug,而是歸咎于 Java 平臺(tái)內(nèi)存模型篓冲。
* 內(nèi)存模型允許所謂的“無序?qū)懭搿崩钇疲@也是這些習(xí)語失敗的一個(gè)主要原因。
* 使用volatile壹将,Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的嗤攻,同時(shí)還會(huì)禁止指令重排序
*/
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
/**
* 第一次檢查:避免線程競爭鎖而造成性能浪費(fèi)
* 只對未初始化的對象加鎖,若已有實(shí)例就不需要加鎖诽俯,優(yōu)化性能
*/
if (uniqueInstance == null) {
// 線程都是對這個(gè)單例類進(jìn)行訪問妇菱,因此需要鎖的是這個(gè)類對象!
synchronized (Singleton.class) {
/**
* 第二次檢查:正真的加鎖判斷
* 能攔截除第一個(gè)獲得對象鎖線程以外的線程暴区,保證線程安全
*/
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
考慮下面的實(shí)現(xiàn)闯团,也就是只使用了一個(gè) if 語句。在 uniqueInstance == null 的情況下仙粱,如果兩個(gè)線程都執(zhí)行了 if 語句房交,那么兩個(gè)線程都會(huì)進(jìn)入 if 語句塊內(nèi)。雖然在 if 語句塊內(nèi)有加鎖操作伐割,但是兩個(gè)線程都會(huì)執(zhí)行 uniqueInstance = new Singleton();
這條語句候味,只是先后的問題,那么就會(huì)進(jìn)行兩次實(shí)例化隔心。因此必須使用雙重校驗(yàn)鎖白群,也就是需要使用兩個(gè) if 語句:第一個(gè) if 語句用來避免 uniqueInstance 已經(jīng)被實(shí)例化之后的加鎖操作,而第二個(gè) if 語句進(jìn)行了加鎖硬霍,所以只能有一個(gè)線程進(jìn)入帜慢,就不會(huì)出現(xiàn) uniqueInstance == null 時(shí)兩個(gè)線程同時(shí)進(jìn)行實(shí)例化操作。
if (uniqueInstance == null) {
synchronized (Singleton.class) {
uniqueInstance = new Singleton();
}
}
uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要的唯卖, uniqueInstance = new Singleton();
這段代碼其實(shí)是分為三步執(zhí)行:
- 為 uniqueInstance 分配內(nèi)存空間
- 初始化 uniqueInstance
- 將 uniqueInstance 指向分配的內(nèi)存地址
但是由于 JVM 具有指令重排的特性粱玲,執(zhí)行順序有可能變成 1>3>2。指令重排在單線程環(huán)境下不會(huì)出現(xiàn)問題拜轨,但是在多線程環(huán)境下會(huì)導(dǎo)致一個(gè)線程獲得還沒有初始化的實(shí)例密幔。例如,線程 T1 執(zhí)行了 1 和 3撩轰,此時(shí) T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空胯甩,因此返回 uniqueInstance,但此時(shí) uniqueInstance 還未被初始化堪嫂。
使用 volatile 可以禁止 JVM 的指令重排偎箫,保證在多線程環(huán)境下也能正常運(yùn)行。
Ⅴ 靜態(tài)內(nèi)部類實(shí)現(xiàn)
當(dāng) Singleton 類被加載時(shí)皆串,靜態(tài)內(nèi)部類 SingletonHolder 沒有被加載進(jìn)內(nèi)存淹办。只有當(dāng)調(diào)用 getUniqueInstance()
方法從而觸發(fā) SingletonHolder.INSTANCE
時(shí) SingletonHolder 才會(huì)被加載,此時(shí)初始化 INSTANCE 實(shí)例恶复,并且 JVM 能確保 INSTANCE 只被實(shí)例化一次怜森。
這種方式不僅具有延遲初始化的好處速挑,而且由 JVM 提供了對線程安全的支持。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
單例模式并不是絕對安全的副硅,可以通過反射來破壞姥宝,反射獲得單例類的構(gòu)造函數(shù),由于該構(gòu)造函數(shù)是private的恐疲,通過setAccessible(true)指示反射的對象在使用時(shí)應(yīng)該取消 Java 語言訪問檢查腊满,使得私有的構(gòu)造函數(shù)能夠被訪問,這樣使得單例模式失效培己。
解決方案:
- 要防止構(gòu)造函數(shù)被成功調(diào)用兩次碳蛋。需要在構(gòu)造函數(shù)中對實(shí)例化次數(shù)進(jìn)行統(tǒng)計(jì),大于一次就拋出異常省咨。
- 枚舉安全類來實(shí)現(xiàn)絕對安全肃弟。
Ⅵ 枚舉實(shí)現(xiàn)
public enum Singleton {
INSTANCE;
private String objName;
public String getObjName() {
return objName;
}
public void setObjName(String objName) {
this.objName = objName;
}
public static void main(String[] args) {
// 單例測試
Singleton firstSingleton = Singleton.INSTANCE;
firstSingleton.setObjName("firstName");
System.out.println(firstSingleton.getObjName());
Singleton secondSingleton = Singleton.INSTANCE;
secondSingleton.setObjName("secondName");
System.out.println(firstSingleton.getObjName());
System.out.println(secondSingleton.getObjName());
// 反射獲取實(shí)例測試
try {
Singleton[] enumConstants = Singleton.class.getEnumConstants();
for (Singleton enumConstant : enumConstants) {
System.out.println(enumConstant.getObjName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
firstName
secondName
secondName
secondName
該實(shí)現(xiàn)可以防止反射攻擊。該實(shí)現(xiàn)在多次序列化和序列化之后零蓉,不會(huì)得到多個(gè)實(shí)例愕乎。而其它實(shí)現(xiàn)需要使用 transient 修飾所有字段,并且實(shí)現(xiàn)序列化和反序列化的方法壁公。
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 在單例模式中感论,活動(dòng)的單例只有一個(gè)實(shí)例,對單例類的所有實(shí)例化得到的都是相同的一個(gè)實(shí)例紊册。這樣就 防止其它對象對自己的實(shí)例化比肄,確保所有的對象都訪問一個(gè)實(shí)例
- 單例模式具有一定的伸縮性,類自己來控制實(shí)例化進(jìn)程囊陡,類就在改變實(shí)例化進(jìn)程上有相應(yīng)的伸縮性芳绩。
- 提供了對唯一實(shí)例的受控訪問。
- 由于在系統(tǒng)內(nèi)存中只存在一個(gè)對象撞反,因此可以 節(jié)約系統(tǒng)資源妥色,當(dāng) 需要頻繁創(chuàng)建和銷毀的對象時(shí)單例模式無疑可以提高系統(tǒng)的性能。
- 允許可變數(shù)目的實(shí)例遏片。
- 避免對共享資源的多重占用嘹害。
缺點(diǎn):
- 不適用于變化的對象,如果同一類型的對象總是要在不同的用例場景發(fā)生變化吮便,單例就會(huì)引起數(shù)據(jù)的錯(cuò)誤笔呀,不能保存彼此的狀態(tài)。
- 由于單利模式中沒有抽象層髓需,因此單例類的擴(kuò)展有很大的困難许师。
- 單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。
- 濫用單例將帶來一些負(fù)面問題微渠,如為了節(jié)省資源將數(shù)據(jù)庫連接池對象設(shè)計(jì)為的單例類搭幻,可能會(huì)導(dǎo)致共享連接池對象的程序過多而出現(xiàn)連接池溢出;如果實(shí)例化的對象長時(shí)間不被利用逞盆,系統(tǒng)會(huì)認(rèn)為是垃圾而被回收檀蹋,這將導(dǎo)致對象狀態(tài)的丟失。
使用注意事項(xiàng):
- 使用時(shí)不能用反射模式創(chuàng)建單例纳击,否則會(huì)實(shí)例化一個(gè)新的對象
- 使用懶單例模式時(shí)注意線程安全問題
- 餓單例模式和懶單例模式構(gòu)造方法都是私有的续扔,因而是不能被繼承的攻臀,有些單例模式可以被繼承(如登記式模式)
適用場景:
單例模式只允許創(chuàng)建一個(gè)對象焕数,因此節(jié)省內(nèi)存,加快對象訪問速度刨啸,因此對象需要被公用的場合適合使用堡赔,如多個(gè)模塊使用同一個(gè)數(shù)據(jù)源連接對象等等。如:
1.需要頻繁實(shí)例化然后銷毀的對象设联。
2.創(chuàng)建對象時(shí)耗時(shí)過多或者耗資源過多善已,但又經(jīng)常用到的對象。
3.有狀態(tài)的工具類對象离例。
4.頻繁訪問數(shù)據(jù)庫或文件的對象换团。
以下都是單例模式的經(jīng)典使用場景:
1.資源共享的情況下,避免由于資源操作時(shí)導(dǎo)致的性能或損耗等宫蛆。如上述中的日志文件艘包,應(yīng)用配置。
2.控制資源的情況下耀盗,方便資源之間的互相通信想虎。如線程池等。
應(yīng)用場景舉例:
1.外部資源:每臺(tái)計(jì)算機(jī)有若干個(gè)打印機(jī)叛拷,但只能有一個(gè)PrinterSpooler舌厨,以避免兩個(gè)打印作業(yè)同時(shí)輸出到打印機(jī)。內(nèi)部資源:大多數(shù)軟件都有一個(gè)(或多個(gè))屬性文件存放系統(tǒng)配置忿薇,這樣的系統(tǒng)應(yīng)該有一個(gè)對象管理這些屬性文件
- Windows的Task Manager(任務(wù)管理器)就是很典型的單例模式(這個(gè)很熟悉吧)裙椭,想想看,是不是呢署浩,你能打開兩個(gè)windows task manager嗎骇陈? 不信你自己試試看哦~
- windows的Recycle Bin(回收站)也是典型的單例應(yīng)用。在整個(gè)系統(tǒng)運(yùn)行過程中瑰抵,回收站一直維護(hù)著僅有的一個(gè)實(shí)例你雌。
- 網(wǎng)站的計(jì)數(shù)器,一般也是采用單例模式實(shí)現(xiàn),否則難以同步婿崭。
- 應(yīng)用程序的日志應(yīng)用拨拓,一般都何用單例模式實(shí)現(xiàn),這一般是由于共享的日志文件一直處于打開狀態(tài)氓栈,因?yàn)橹荒苡幸粋€(gè)實(shí)例去操作渣磷,否則內(nèi)容不好追加。
- Web應(yīng)用的配置對象的讀取授瘦,一般也應(yīng)用單例模式醋界,這個(gè)是由于配置文件是共享的資源。
- 數(shù)據(jù)庫連接池的設(shè)計(jì)一般也是采用單例模式提完,因?yàn)閿?shù)據(jù)庫連接是一種數(shù)據(jù)庫資源形纺。數(shù)據(jù)庫軟件系統(tǒng)中使用數(shù)據(jù)庫連接池,主要是節(jié)省打開或者關(guān)閉數(shù)據(jù)庫連接所引起的效率損耗徒欣,這種效率上的損耗還是非常昂貴的逐样,因?yàn)楹斡脝卫J絹砭S護(hù),就可以大大降低這種損耗打肝。
- 多線程的線程池的設(shè)計(jì)一般也是采用單例模式脂新,這是由于線程池要方便對池中的線程進(jìn)行控制。
- 操作系統(tǒng)的文件系統(tǒng)粗梭,也是大的單例模式實(shí)現(xiàn)的具體例子争便,一個(gè)操作系統(tǒng)只能有一個(gè)文件系統(tǒng)。
- HttpApplication 也是單位例的典型應(yīng)用断医。熟悉ASP.Net(IIS)的整個(gè)請求生命周期的人應(yīng)該知道HttpApplication也是單例模式滞乙,所有的HttpModule都共享一個(gè)HttpApplication實(shí)例.
應(yīng)用舉例
- Logger Classes
- Configuration Classes
- Accesing resources in shared mode
- Factories implemented as Singletons
JDK中使用場景
2 原型模式(Prototype)
Intent
使用原型實(shí)例指定要?jiǎng)?chuàng)建對象的類型,通過復(fù)制這個(gè)原型來創(chuàng)建新對象孩锡。
Class Diagram
Implementation
public abstract class Prototype {
abstract Prototype myClone();
}
public class ConcretePrototype extends Prototype {
private String filed;
public ConcretePrototype(String filed) {
this.filed = filed;
}
@Override
Prototype myClone() {
return new ConcretePrototype(filed);
}
@Override
public String toString() {
return filed;
}
}
public class Client {
public static void main(String[] args) {
Prototype prototype = new ConcretePrototype("abc");
Prototype clone = prototype.myClone();
System.out.println(clone.toString());
}
}
abc
JDK
3 抽象工廠(Abstract Factory)
Intent
提供一個(gè)接口酷宵,用于創(chuàng)建 相關(guān)的對象家族 。
Class Diagram
抽象工廠模式創(chuàng)建的是對象家族躬窜,也就是很多對象而不是一個(gè)對象浇垦,并且這些對象是相關(guān)的,也就是說必須一起創(chuàng)建出來荣挨。而工廠方法模式只是用于創(chuàng)建一個(gè)對象男韧,這和抽象工廠模式有很大不同。
抽象工廠模式用到了工廠方法模式來創(chuàng)建單一對象默垄,AbstractFactory 中的 createProductA() 和 createProductB() 方法都是讓子類來實(shí)現(xiàn)此虑,這兩個(gè)方法單獨(dú)來看就是在創(chuàng)建一個(gè)對象,這符合工廠方法模式的定義口锭。
至于創(chuàng)建對象的家族這一概念是在 Client 體現(xiàn)朦前,Client 要通過 AbstractFactory 同時(shí)調(diào)用兩個(gè)方法來創(chuàng)建出兩個(gè)對象介杆,在這里這兩個(gè)對象就有很大的相關(guān)性,Client 需要同時(shí)創(chuàng)建出這兩個(gè)對象韭寸。
從高層次來看春哨,抽象工廠使用了組合,即 Cilent 組合了 AbstractFactory恩伺,而工廠方法模式使用了繼承赴背。
Implementation
public class AbstractProductA {
}
public class AbstractProductB {
}
public class ProductA1 extends AbstractProductA {
}
public class ProductA2 extends AbstractProductA {
}
public class ProductB1 extends AbstractProductB {
}
public class ProductB2 extends AbstractProductB {
}
public abstract class AbstractFactory {
abstract AbstractProductA createProductA();
abstract AbstractProductB createProductB();
}
public class ConcreteFactory1 extends AbstractFactory {
AbstractProductA createProductA() {
return new ProductA1();
}
AbstractProductB createProductB() {
return new ProductB1();
}
}
public class ConcreteFactory2 extends AbstractFactory {
AbstractProductA createProductA() {
return new ProductA2();
}
AbstractProductB createProductB() {
return new ProductB2();
}
}
public class Client {
public static void main(String[] args) {
AbstractFactory abstractFactory = new ConcreteFactory1();
AbstractProductA productA = abstractFactory.createProductA();
AbstractProductB productB = abstractFactory.createProductB();
// do something with productA and productB
}
}
JDK
- javax.xml.parsers.DocumentBuilderFactory
- javax.xml.transform.TransformerFactory
- javax.xml.xpath.XPathFactory
4 工廠方法(Factory Method)
Intent
定義了一個(gè)創(chuàng)建對象的接口,但由子類決定要實(shí)例化哪個(gè)類晶渠。工廠方法把實(shí)例化操作推遲到子類凰荚。
Class Diagram
在簡單工廠中,創(chuàng)建對象的是另一個(gè)類褒脯,而在工廠方法中便瑟,是由子類來創(chuàng)建對象。
下圖中憨颠,F(xiàn)actory 有一個(gè) doSomething() 方法胳徽,這個(gè)方法需要用到一個(gè)產(chǎn)品對象积锅,這個(gè)產(chǎn)品對象由 factoryMethod() 方法創(chuàng)建爽彤。該方法是抽象的,需要由子類去實(shí)現(xiàn)缚陷。
Implementation
public abstract class Factory {
abstract public Product factoryMethod();
public void doSomething() {
Product product = factoryMethod();
// do something with the product
}
}
public class ConcreteFactory extends Factory {
public Product factoryMethod() {
return new ConcreteProduct();
}
}
public class ConcreteFactory1 extends Factory {
public Product factoryMethod() {
return new ConcreteProduct1();
}
}
public class ConcreteFactory2 extends Factory {
public Product factoryMethod() {
return new ConcreteProduct2();
}
}
JDK
- java.util.Calendar
- java.util.ResourceBundle
- java.text.NumberFormat
- java.nio.charset.Charset
- java.net.URLStreamHandlerFactory
- java.util.EnumSet
- javax.xml.bind.JAXBContext
5 建造者/生成器模式(Builder)
Intent
封裝一個(gè)對象的構(gòu)造過程适篙,并允許按步驟構(gòu)造。
Class Diagram
Implementation
以下是一個(gè)簡易的 StringBuilder 實(shí)現(xiàn)箫爷,參考了 JDK 1.8 源碼嚷节。
public class AbstractStringBuilder {
protected char[] value;
protected int count;
public AbstractStringBuilder(int capacity) {
count = 0;
value = new char[capacity];
}
public AbstractStringBuilder append(char c) {
ensureCapacityInternal(count + 1);
value[count++] = c;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
}
public class StringBuilder extends AbstractStringBuilder {
public StringBuilder() {
super(16);
}
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
}
public class Client {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
final int count = 26;
for (int i = 0; i < count; i++) {
sb.append((char) ('a' + i));
}
System.out.println(sb.toString());
}
}
abcdefghijklmnopqrstuvwxyz
JDK
- java.lang.StringBuilder
- java.nio.ByteBuffer
- java.lang.StringBuffer
- java.lang.Appendable
- Apache Camel builders