1 場(chǎng)景問(wèn)題#
1.1 讀取配置文件的內(nèi)容##
考慮這樣一個(gè)應(yīng)用植康,讀取配置文件的內(nèi)容另凌。
很多應(yīng)用項(xiàng)目,都有與應(yīng)用相關(guān)的配置文件劈榨,這些配置文件多是由項(xiàng)目開發(fā)人員自定義的,在里面定義一些應(yīng)用需要的參數(shù)數(shù)據(jù)晦嵌。當(dāng)然在實(shí)際的項(xiàng)目中同辣,這種配置文件多采用xml格式的。也有采用properties格式的耍铜,畢竟使用Java來(lái)讀取properties格式的配置文件比較簡(jiǎn)單邑闺。
現(xiàn)在要讀取配置文件的內(nèi)容跌前,該如何實(shí)現(xiàn)呢棕兼?
1.2 不用模式的解決方案##
有些朋友會(huì)想,要讀取配置文件的內(nèi)容抵乓,這也不是個(gè)什么困難的事情伴挚,直接讀取文件的內(nèi)容,然后把文件內(nèi)容存放在相應(yīng)的數(shù)據(jù)對(duì)象里面就可以了灾炭。真的這么簡(jiǎn)單嗎茎芋?先實(shí)現(xiàn)看看吧。
為了示例簡(jiǎn)單蜈出,假設(shè)系統(tǒng)是采用的properties格式的配置文件田弥。
- 那么直接使用Java來(lái)讀取配置文件,示例代碼如下:
/**
* 讀取應(yīng)用配置文件
*/
public class AppConfig {
/**
* 用來(lái)存放配置文件中參數(shù)A的值
*/
private String parameterA;
/**
* 用來(lái)存放配置文件中參數(shù)B的值
*/
private String parameterB;
public String getParameterA() {
return parameterA;
}
public String getParameterB() {
return parameterB;
}
/**
* 構(gòu)造方法
*/
public AppConfig(){
//調(diào)用讀取配置文件的方法
readConfig();
}
/**
* 讀取配置文件铡原,把配置文件中的內(nèi)容讀出來(lái)設(shè)置到屬性上
*/
private void readConfig(){
Properties p = new Properties();
InputStream in = null;
try {
in = AppConfig.class.getResourceAsStream("AppConfig.properties");
p.load(in);
//把配置文件中的內(nèi)容讀出來(lái)設(shè)置到屬性上
this.parameterA = p.getProperty("paramA");
this.parameterB = p.getProperty("paramB");
} catch (IOException e) {
System.out.println("裝載配置文件出錯(cuò)了偷厦,具體堆棧信息如下:");
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意:只有訪問(wèn)參數(shù)的方法,沒(méi)有設(shè)置參數(shù)的方法燕刻。
- 應(yīng)用的配置文件只泼,名字是AppConfig.properties,放在AppConfig相同的包里面卵洗,簡(jiǎn)單示例如下:
paramA=a
paramB=b
- 寫個(gè)客戶端來(lái)測(cè)試一下请唱,示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建讀取應(yīng)用配置的對(duì)象
AppConfig config = new AppConfig();
String paramA = config.getParameterA();
String paramB = config.getParameterB();
System.out.println("paramA="+paramA+",paramB="+paramB);
}
}
1.3 有何問(wèn)題##
上面的實(shí)現(xiàn)很簡(jiǎn)單嘛,很容易的就實(shí)現(xiàn)了要求的功能过蹂。仔細(xì)想想十绑,有沒(méi)有什么問(wèn)題呢?
看看客戶端使用這個(gè)類的地方酷勺,是通過(guò)new一個(gè)AppConfig的實(shí)例來(lái)得到一個(gè)操作配置文件內(nèi)容的對(duì)象本橙。如果在系統(tǒng)運(yùn)行中,有很多地方都需要使用配置文件的內(nèi)容鸥印,也就是很多地方都需要?jiǎng)?chuàng)建AppConfig這個(gè)對(duì)象的實(shí)例勋功。
換句話說(shuō)坦报,在系統(tǒng)運(yùn)行期間,系統(tǒng)中會(huì)存在很多個(gè)AppConfig的實(shí)例對(duì)象狂鞋,這有什么問(wèn)題嗎片择?
當(dāng)然有問(wèn)題了,試想一下骚揍,每一個(gè)AppConfig實(shí)例對(duì)象字管,里面都封裝著配置文件的內(nèi)容,系統(tǒng)中有多個(gè)AppConfig實(shí)例對(duì)象信不,也就是說(shuō)系統(tǒng)中會(huì)同時(shí)存在多份配置文件的內(nèi)容嘲叔,這會(huì)嚴(yán)重浪費(fèi)內(nèi)存資源。如果配置文件內(nèi)容較少抽活,問(wèn)題還小一點(diǎn)硫戈,如果配置文件內(nèi)容本來(lái)就多的話,對(duì)于系統(tǒng)資源的浪費(fèi)問(wèn)題就大了下硕。事實(shí)上丁逝,對(duì)于AppConfig這種類,在運(yùn)行期間梭姓,只需要一個(gè)實(shí)例對(duì)象就夠了霜幼。
把上面的描述進(jìn)一步抽象一下,問(wèn)題就出來(lái)了:在一個(gè)系統(tǒng)運(yùn)行期間誉尖,某個(gè)類只需要一個(gè)類實(shí)例就可以了罪既,那么應(yīng)該怎么實(shí)現(xiàn)呢?
2 解決方案#
2.1 單例模式來(lái)解決##
用來(lái)解決上述問(wèn)題的一個(gè)合理的解決方案就是單例模式铡恕。那么什么是單例模式呢琢感?
- 單例模式定義
保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)没咙。
- 應(yīng)用單例模式來(lái)解決的思路
仔細(xì)分析上面的問(wèn)題猩谊,現(xiàn)在一個(gè)類能夠被創(chuàng)建多個(gè)實(shí)例,問(wèn)題的根源在于類的構(gòu)造方法是公開的祭刚,也就是可以讓類的外部來(lái)通過(guò)構(gòu)造方法創(chuàng)建多個(gè)實(shí)例牌捷。換句話說(shuō),只要類的構(gòu)造方法能讓類的外部訪問(wèn)涡驮,就沒(méi)有辦法去控制外部來(lái)創(chuàng)建這個(gè)類的實(shí)例個(gè)數(shù)暗甥。
要想控制一個(gè)類只被創(chuàng)建一個(gè)實(shí)例,那么首要的問(wèn)題就是要把創(chuàng)建實(shí)例的權(quán)限收回來(lái)捉捅,讓類自身來(lái)負(fù)責(zé)自己類實(shí)例的創(chuàng)建工作撤防,然后由這個(gè)類來(lái)提供外部可以訪問(wèn)這個(gè)類實(shí)例的方法,這就是單例模式的實(shí)現(xiàn)方式棒口。
2.2 模式結(jié)構(gòu)和說(shuō)明##
單例模式結(jié)構(gòu)如圖所示:
Singleton:負(fù)責(zé)創(chuàng)建Singleton類自己的唯一實(shí)例寄月,并提供一個(gè)getInstance的方法辜膝,讓外部來(lái)訪問(wèn)這個(gè)類的唯一實(shí)例。
2.3 單例模式示例代碼##
在Java中漾肮,單例模式的實(shí)現(xiàn)又分為兩種厂抖,一種稱為懶漢式,一種稱為餓漢式克懊,其實(shí)就是在具體創(chuàng)建對(duì)象實(shí)例的處理上忱辅,有不同的實(shí)現(xiàn)方式。下面分別來(lái)看這兩種實(shí)現(xiàn)方式的代碼示例谭溉。為何這么寫墙懂,具體的在后面再講述。
- 懶漢式實(shí)現(xiàn)扮念,示例代碼如下:
/**
* 懶漢式單例實(shí)現(xiàn)的示例
*/
public class Singleton {
/**
* 定義一個(gè)變量來(lái)存儲(chǔ)創(chuàng)建好的類實(shí)例
*/
private static Singleton uniqueInstance = null;
/**
* 私有化構(gòu)造方法燕差,好在內(nèi)部控制創(chuàng)建實(shí)例的數(shù)目
*/
private Singleton(){
//
}
/**
* 定義一個(gè)方法來(lái)為客戶端提供類實(shí)例
* @return 一個(gè)Singleton的實(shí)例
*/
public static synchronized Singleton getInstance(){
//判斷存儲(chǔ)實(shí)例的變量是否有值
if(uniqueInstance == null){
//如果沒(méi)有板丽,就創(chuàng)建一個(gè)類實(shí)例旺入,并把值賦值給存儲(chǔ)類實(shí)例的變量
uniqueInstance = new Singleton();
}
//如果有值苞氮,那就直接使用
return uniqueInstance;
}
/**
* 示意方法谈为,單例可以有自己的操作
*/
public void singletonOperation(){
//功能處理
}
/**
* 示意屬性旅挤,單例可以有自己的屬性
*/
private String singletonData;
/**
* 示意方法,讓外部通過(guò)這些方法來(lái)訪問(wèn)屬性的值
* @return 屬性的值
*/
public String getSingletonData(){
return singletonData;
}
}
- 餓漢式實(shí)現(xiàn)伞鲫,示例代碼如下:
/**
* 餓漢式單例實(shí)現(xiàn)的示例
*/
public class Singleton {
/**
* 定義一個(gè)變量來(lái)存儲(chǔ)創(chuàng)建好的類實(shí)例粘茄,直接在這里創(chuàng)建類實(shí)例,只會(huì)創(chuàng)建一次
*/
private static Singleton uniqueInstance = new Singleton();
/**
* 私有化構(gòu)造方法秕脓,好在內(nèi)部控制創(chuàng)建實(shí)例的數(shù)目
*/
private Singleton(){
//
}
/**
* 定義一個(gè)方法來(lái)為客戶端提供類實(shí)例
* @return 一個(gè)Singleton的實(shí)例
*/
public static Singleton getInstance(){
//直接使用已經(jīng)創(chuàng)建好的實(shí)例
return uniqueInstance;
}
/**
* 示意方法柒瓣,單例可以有自己的操作
*/
public void singletonOperation(){
//功能處理
}
/**
* 示意屬性,單例可以有自己的屬性
*/
private String singletonData;
/**
* 示意方法吠架,讓外部通過(guò)這些方法來(lái)訪問(wèn)屬性的值
* @return 屬性的值
*/
public String getSingletonData(){
return singletonData;
}
}
2.4 使用單例模式重寫示例##
要使用單例模式來(lái)重寫示例芙贫,由于單例模式有兩種實(shí)現(xiàn)方式,這里選一種來(lái)實(shí)現(xiàn)就好了傍药,就選擇餓漢式的實(shí)現(xiàn)方式來(lái)重寫示例吧磺平。采用餓漢式的實(shí)現(xiàn)方式來(lái)重寫實(shí)例的示例代碼如下:
/**
* 讀取應(yīng)用配置文件,單例實(shí)現(xiàn)
*/
public class AppConfig {
/**
* 定義一個(gè)變量來(lái)存儲(chǔ)創(chuàng)建好的類實(shí)例拐辽,直接在這里創(chuàng)建類實(shí)例拣挪,只會(huì)創(chuàng)建一次
*/
private static AppConfig instance = new AppConfig();
/**
* 定義一個(gè)方法來(lái)為客戶端提供AppConfig類的實(shí)例
* @return 一個(gè)AppConfig的實(shí)例
*/
public static AppConfig getInstance(){
return instance;
}
/**
* 用來(lái)存放配置文件中參數(shù)A的值
*/
private String parameterA;
/**
* 用來(lái)存放配置文件中參數(shù)B的值
*/
private String parameterB;
public String getParameterA() {
return parameterA;
}
public String getParameterB() {
return parameterB;
}
/**
* 私有化構(gòu)造方法
*/
private AppConfig(){
//調(diào)用讀取配置文件的方法
readConfig();
}
/**
* 讀取配置文件,把配置文件中的內(nèi)容讀出來(lái)設(shè)置到屬性上
*/
private void readConfig(){
Properties p = new Properties();
InputStream in = null;
try {
in = AppConfig.class.getResourceAsStream("AppConfig.properties");
p.load(in);
//把配置文件中的內(nèi)容讀出來(lái)設(shè)置到屬性上
this.parameterA = p.getProperty("paramA");
this.parameterB = p.getProperty("paramB");
} catch (IOException e) {
System.out.println("裝載配置文件出錯(cuò)了俱诸,具體堆棧信息如下:");
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
當(dāng)然菠劝,測(cè)試的客戶端也需要相應(yīng)的變化,示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建讀取應(yīng)用配置的對(duì)象
AppConfig config = AppConfig.getInstance();
String paramA = config.getParameterA();
String paramB = config.getParameterB();
System.out.println("paramA="+paramA+",paramB="+paramB);
}
}
3 模式講解#
3.1 認(rèn)識(shí)單例模式##
- 單例模式的功能
單例模式的功能是用來(lái)保證這個(gè)類在運(yùn)行期間只會(huì)被創(chuàng)建一個(gè)類實(shí)例睁搭,另外單例模式還提供了一個(gè)全局唯一訪問(wèn)這個(gè)類實(shí)例的訪問(wèn)點(diǎn)赶诊,就是那個(gè)getInstance的方法笼平。不管采用懶漢式還是餓漢式的實(shí)現(xiàn)方式,這個(gè)全局訪問(wèn)點(diǎn)是一樣的舔痪。
對(duì)于單例模式而言出吹,不管采用何種實(shí)現(xiàn)方式,它都是只關(guān)心類實(shí)例的創(chuàng)建問(wèn)題辙喂,并不關(guān)心具體的業(yè)務(wù)功能捶牢。
- 單例模式的范圍
也就是在多大范圍內(nèi)是單例呢?
觀察上面的實(shí)現(xiàn)可以知道巍耗,目前Java里面實(shí)現(xiàn)的單例是一個(gè)ClassLoader及其子ClassLoader的范圍秋麸。因?yàn)橐粋€(gè)ClassLoader在裝載餓漢式實(shí)現(xiàn)的單例類的時(shí)候就會(huì)創(chuàng)建一個(gè)類的實(shí)例。
這就意味著如果一個(gè)虛擬機(jī)里面有很多個(gè)ClassLoader炬太,而且這些ClassLoader都裝載某個(gè)類的話灸蟆,就算這個(gè)類是單例,它也會(huì)產(chǎn)生很多個(gè)實(shí)例亲族。當(dāng)然炒考,如果一個(gè)機(jī)器上有多個(gè)虛擬機(jī),那么每個(gè)虛擬機(jī)里面都應(yīng)該至少有一個(gè)這個(gè)類的實(shí)例霎迫,也就是說(shuō)整個(gè)機(jī)器上就有很多個(gè)實(shí)例斋枢,更不會(huì)是單例了。
另外請(qǐng)注意一點(diǎn)知给,這里討論的單例模式并不適用于集群環(huán)境瓤帚,對(duì)于集群環(huán)境下的單例這里不去討論,那不屬于這里的內(nèi)容范圍涩赢。
- 單例模式的命名
一般建議單例模式的方法命名為:getInstance()戈次,這個(gè)方法的返回類型肯定是單例類的類型了。getInstance方法可以有參數(shù)筒扒,這些參數(shù)可能是創(chuàng)建類實(shí)例所需要的參數(shù)怯邪,當(dāng)然,大多數(shù)情況下是不需要的花墩。
單例模式的名稱:?jiǎn)卫渭误w等等观游,翻譯的不同搂捧,都是指的同一個(gè)模式。
3.2 懶漢式和餓漢式實(shí)現(xiàn)##
前面提到了單例模式有兩種典型的解決方案懂缕,一種叫懶漢式允跑,一種叫餓漢式,這兩種方式究竟是如何實(shí)現(xiàn)的,下面分別來(lái)看看聋丝。為了看得更清晰一點(diǎn)索烹,只是實(shí)現(xiàn)基本的單例控制部分,不再提供示例的屬性和方法了弱睦;而且暫時(shí)也不去考慮線程安全的問(wèn)題百姓,這個(gè)問(wèn)題在后面會(huì)重點(diǎn)分析。
第一種方案 懶漢式
- 私有化構(gòu)造方法:
要想在運(yùn)行期間控制某一個(gè)類的實(shí)例只有一個(gè)况木,那首先的任務(wù)就是要控制創(chuàng)建實(shí)例的地方垒拢,也就是不能隨隨便便就可以創(chuàng)建類實(shí)例,否則就無(wú)法控制創(chuàng)建的實(shí)例個(gè)數(shù)了』鹁現(xiàn)在是讓使用類的地方來(lái)創(chuàng)建類實(shí)例求类,也就是在類外部來(lái)創(chuàng)建類實(shí)例。那么怎樣才能讓類的外部不能創(chuàng)建一個(gè)類的實(shí)例呢屹耐?很簡(jiǎn)單尸疆,私有化構(gòu)造方法就可以了!
private Singleton() {
}
- 提供獲取實(shí)例的方法
構(gòu)造方法被私有化了惶岭,外部使用這個(gè)類的地方不干了寿弱,外部創(chuàng)建不了類實(shí)例就沒(méi)有辦法調(diào)用這個(gè)對(duì)象的方法,就實(shí)現(xiàn)不了功能處理按灶,這可不行症革。經(jīng)過(guò)思考,單例模式?jīng)Q定讓這個(gè)類提供一個(gè)方法來(lái)返回類的實(shí)例兆衅,好讓外面使用地沮。示例代碼如下:
public Singleton getInstance() {
}
- 把獲取實(shí)例的方法變成靜態(tài)的
又有新的問(wèn)題了,獲取對(duì)象實(shí)例的這個(gè)方法是個(gè)實(shí)例方法羡亩,也就是說(shuō)客戶端要想調(diào)用這個(gè)方法,需要先得到類實(shí)例危融,然后才可以調(diào)用畏铆,可是這個(gè)方法就是為了得到類實(shí)例,這樣一來(lái)不就形成一個(gè)死循環(huán)了嗎吉殃?這不就是典型的“先有雞還是先有蛋的問(wèn)題”嘛辞居。
解決方法也很簡(jiǎn)單,在方法上加上static蛋勺,這樣就可以直接通過(guò)類來(lái)調(diào)用這個(gè)方法瓦灶,而不需要先得到類實(shí)例了,示例代碼如下:
public static Singleton getInstance() {
}
- 定義存儲(chǔ)實(shí)例的屬性
方法定義好了抱完,那么方法內(nèi)部如何實(shí)現(xiàn)呢贼陶?如果直接創(chuàng)建實(shí)例并返回,這樣行不行呢?示例代碼如下:
public static Singleton getInstance(){
return new Singleton();
}
當(dāng)然不行了碉怔,如果每次客戶端訪問(wèn)都這樣直接new一個(gè)實(shí)例烘贴,那肯定會(huì)有多個(gè)實(shí)例,根本實(shí)現(xiàn)不了單例的功能撮胧。
怎么辦呢桨踪?單例模式想到了一個(gè)辦法,那就是用一個(gè)屬性來(lái)記錄自己創(chuàng)建好的類實(shí)例芹啥,當(dāng)?shù)谝淮蝿?chuàng)建過(guò)后锻离,就把這個(gè)實(shí)例保存下來(lái),以后就可以復(fù)用這個(gè)實(shí)例墓怀,而不是重復(fù)創(chuàng)建對(duì)象實(shí)例了纳账。示例代碼如下:
private Singleton instance = null;
- 把這個(gè)屬性也定義成靜態(tài)的
這個(gè)屬性變量應(yīng)該在什么地方用呢?肯定是第一次創(chuàng)建類實(shí)例的地方捺疼,也就是在前面那個(gè)返回對(duì)象實(shí)例的靜態(tài)方法里面使用疏虫。
由于要在一個(gè)靜態(tài)方法里面使用,所以這個(gè)屬性被迫成為一個(gè)類變量啤呼,要強(qiáng)制加上static卧秘,也就是說(shuō),這里并沒(méi)有使用static的特性官扣。示例代碼如下:
private static Singleton instance = null;
- 實(shí)現(xiàn)控制實(shí)例的創(chuàng)建
現(xiàn)在應(yīng)該到getInstance方法里面實(shí)現(xiàn)控制實(shí)例創(chuàng)建了翅敌,控制的方式很簡(jiǎn)單,只要先判斷一下惕蹄,是否已經(jīng)創(chuàng)建過(guò)實(shí)例了蚯涮。如何判斷?那就看存放實(shí)例的屬性是否有值卖陵,如果有值遭顶,說(shuō)明已經(jīng)創(chuàng)建過(guò)了,如果沒(méi)有值泪蔫,那就是應(yīng)該創(chuàng)建一個(gè)棒旗,示例代碼如下:
public static Singleton getInstance() {
//先判斷instance是否有值
if (instance == null) {
//如果沒(méi)有值,說(shuō)明還沒(méi)有創(chuàng)建過(guò)實(shí)例撩荣,那就創(chuàng)建一個(gè)
//并把這個(gè)實(shí)例設(shè)置給instance
instance = new Singleton ();
}
//如果有值铣揉,或者是創(chuàng)建了值,那就直接使用
return instance;
}
- 完整的實(shí)現(xiàn)
至此餐曹,成功解決了:在運(yùn)行期間逛拱,控制某個(gè)類只被創(chuàng)建一個(gè)實(shí)例的要求。完整的代碼如下台猴,為了大家好理解朽合,用注釋標(biāo)示了代碼的先后順序俱两,示例代碼如下:
public class Singleton {
//4:定義一個(gè)變量來(lái)存儲(chǔ)創(chuàng)建好的類實(shí)例
//5:因?yàn)檫@個(gè)變量要在靜態(tài)方法中使用,所以需要加上static修飾
private static Singleton instance = null;
//1:私有化構(gòu)造方法旁舰,好在內(nèi)部控制創(chuàng)建實(shí)例的數(shù)目
private Singleton(){
}
//2:定義一個(gè)方法來(lái)為客戶端提供類實(shí)例
//3:這個(gè)方法需要定義成類方法锋华,也就是要加static
public static Singleton getInstance(){
//6:判斷存儲(chǔ)實(shí)例的變量是否有值
if(instance == null){
//6.1:如果沒(méi)有,就創(chuàng)建一個(gè)類實(shí)例箭窜,并把值賦值給存儲(chǔ)類實(shí)例的變量
instance = new Singleton();
}
//6.2:如果有值毯焕,那就直接使用
return instance;
}
}
第二種方案 餓漢式
這種方案跟第一種方案相比,前面的私有化構(gòu)造方法磺樱,提供靜態(tài)的getInstance方法來(lái)返回實(shí)例等步驟都一樣纳猫。差別在如何實(shí)現(xiàn)getInstance方法,在這個(gè)地方竹捉,單例模式還想到了另外一種方法來(lái)實(shí)現(xiàn)getInstance方法芜辕。
不就是要控制只創(chuàng)造一個(gè)實(shí)例嗎?那么有沒(méi)有什么現(xiàn)成的解決辦法呢块差?很快侵续,單例模式回憶起了Java中static的特性:
static變量在類裝載的時(shí)候進(jìn)行初始化。
多個(gè)實(shí)例的static變量會(huì)共享同一塊內(nèi)存區(qū)域憨闰。
這就意味著状蜗,在Java中,static變量只會(huì)被初始化一次鹉动,就是在類裝載的時(shí)候轧坎,而且多個(gè)實(shí)例都會(huì)共享這個(gè)內(nèi)存空間,這不就是單例模式要實(shí)現(xiàn)的功能嗎泽示?真是得來(lái)全不費(fèi)功夫啊缸血。根據(jù)這些知識(shí),寫出了第二種解決方案的代碼械筛,示例代碼如下:
public class Singleton {
//4:定義一個(gè)靜態(tài)變量來(lái)存儲(chǔ)創(chuàng)建好的類實(shí)例
//直接在這里創(chuàng)建類實(shí)例捎泻,只會(huì)創(chuàng)建一次
private static Singleton instance = new Singleton();
//1:私有化構(gòu)造方法,好在內(nèi)部控制創(chuàng)建實(shí)例的數(shù)目
private Singleton(){
}
//2:定義一個(gè)方法來(lái)為客戶端提供類實(shí)例
//3:這個(gè)方法需要定義成類方法变姨,也就是要加static
//這個(gè)方法里面就不需要控制代碼了
public static Singleton getInstance(){
//5:直接使用已經(jīng)創(chuàng)建好的實(shí)例
return instance;
}
}
不管是采用哪一種方式族扰,在運(yùn)行期間,都只會(huì)生成一個(gè)實(shí)例定欧,而訪問(wèn)這些類的一個(gè)全局訪問(wèn)點(diǎn),就是那個(gè)靜態(tài)的getInstance方法怒竿。
單例模式的調(diào)用順序示意圖
先看懶漢式的調(diào)用順序砍鸠,如圖所示:
餓漢式的調(diào)用順序,如圖所示:
3.3 延遲加載的思想##
單例模式的懶漢式實(shí)現(xiàn)方式體現(xiàn)了延遲加載的思想耕驰,什么是延遲加載呢爷辱?
通俗點(diǎn)說(shuō),就是一開始不要加載資源或者數(shù)據(jù),一直等饭弓,等到馬上就要使用這個(gè)資源或者數(shù)據(jù)了双饥,躲不過(guò)去了才加載,所以也稱Lazy Load弟断,不是懶惰啊咏花,是“延遲加載”,這在實(shí)際開發(fā)中是一種很常見(jiàn)的思想阀趴,盡可能的節(jié)約資源昏翰。
體現(xiàn)在什么地方呢?看如下代碼:
3.4 緩存的思想##
單例模式的懶漢式實(shí)現(xiàn)還體現(xiàn)了緩存的思想刘急,緩存也是實(shí)際開發(fā)中非常常見(jiàn)的功能棚菊。
簡(jiǎn)單講就是,如果某些資源或者數(shù)據(jù)會(huì)被頻繁的使用叔汁,而這些資源或數(shù)據(jù)存儲(chǔ)在系統(tǒng)外部统求,比如數(shù)據(jù)庫(kù)、硬盤文件等据块,那么每次操作這些數(shù)據(jù)的時(shí)候都從數(shù)據(jù)庫(kù)或者硬盤上去獲取码邻,速度會(huì)很慢,會(huì)造成性能問(wèn)題瑰钮。
一個(gè)簡(jiǎn)單的解決方法就是:把這些數(shù)據(jù)緩存到內(nèi)存里面冒滩,每次操作的時(shí)候,先到內(nèi)存里面找浪谴,看有沒(méi)有這些數(shù)據(jù)开睡,如果有,那么就直接使用苟耻,如果沒(méi)有那么就獲取它篇恒,并設(shè)置到緩存中,下一次訪問(wèn)的時(shí)候就可以直接從內(nèi)存中獲取了凶杖。從而節(jié)省大量的時(shí)間胁艰,當(dāng)然,緩存是一種典型的空間換時(shí)間的方案智蝠。
緩存在單例模式的實(shí)現(xiàn)中怎么體現(xiàn)的呢腾么?
3.5 Java中緩存的基本實(shí)現(xiàn)##
引申一下,看看在Java開發(fā)中的緩存的基本實(shí)現(xiàn)杈湾,在Java中最常見(jiàn)的一種實(shí)現(xiàn)緩存的方式就是使用Map解虱,基本的步驟是:
先到緩存里面查找,看看是否存在需要使用的數(shù)據(jù)
如果沒(méi)有找到漆撞,那么就創(chuàng)建一個(gè)滿足要求的數(shù)據(jù)殴泰,然后把這個(gè)數(shù)據(jù)設(shè)置回到緩存中于宙,以備下次使用
如果找到了相應(yīng)的數(shù)據(jù),或者是創(chuàng)建了相應(yīng)的數(shù)據(jù)悍汛,那就直接使用這個(gè)數(shù)據(jù)捞魁。
還是看看示例吧,示例代碼如下:
/**
* Java中緩存的基本實(shí)現(xiàn)示例
*/
public class JavaCache {
/**
* 緩存數(shù)據(jù)的容器离咐,定義成Map是方便訪問(wèn)谱俭,直接根據(jù)Key就可以獲取Value了
* key選用String是為了簡(jiǎn)單,方便演示
*/
private Map<String,Object> map = new HashMap<String,Object>();
/**
* 從緩存中獲取值
* @param key 設(shè)置時(shí)候的key值
* @return key對(duì)應(yīng)的Value值
*/
public Object getValue(String key){
//先從緩存里面取值
Object obj = map.get(key);
//判斷緩存里面是否有值
if(obj == null){
//如果沒(méi)有健霹,那么就去獲取相應(yīng)的數(shù)據(jù)旺上,比如讀取數(shù)據(jù)庫(kù)或者文件
//這里只是演示,所以直接寫個(gè)假的值
obj = key+",value";
//把獲取的值設(shè)置回到緩存里面
map.put(key, obj);
}
//如果有值了糖埋,就直接返回使用
return obj;
}
}
這里只是緩存的基本實(shí)現(xiàn)宣吱,還有很多功能都沒(méi)有考慮,比如緩存的清除瞳别,緩存的同步等等征候。當(dāng)然,Java的緩存還有很多實(shí)現(xiàn)方式祟敛,也是非常復(fù)雜的疤坝,現(xiàn)在有很多專業(yè)的緩存框架,更多緩存的知識(shí)馆铁,這里就不再去討論了跑揉。
3.6 利用緩存來(lái)實(shí)現(xiàn)單例模式##
其實(shí)應(yīng)用Java緩存的知識(shí),也可以變相實(shí)現(xiàn)Singleton模式埠巨,算是一個(gè)模擬實(shí)現(xiàn)吧历谍。每次都先從緩存中取值,只要?jiǎng)?chuàng)建一次對(duì)象實(shí)例過(guò)后辣垒,就設(shè)置了緩存的值望侈,那么下次就不用再創(chuàng)建了。
雖然不是很標(biāo)準(zhǔn)的做法勋桶,但是同樣可以實(shí)現(xiàn)單例模式的功能脱衙,為了簡(jiǎn)單,先不去考慮多線程的問(wèn)題例驹,示例代碼如下:
/**
* 使用緩存來(lái)模擬實(shí)現(xiàn)單例
*/
public class Singleton {
/**
* 定義一個(gè)缺省的key值捐韩,用來(lái)標(biāo)識(shí)在緩存中的存放
*/
private final static String DEFAULT_KEY = "One";
/**
* 緩存實(shí)例的容器
*/
private static Map<String,Singleton> map = new HashMap<String,Singleton>();
/**
* 私有化構(gòu)造方法
*/
private Singleton(){
//
}
public static Singleton getInstance(){
//先從緩存中獲取
Singleton instance = (Singleton)map.get(DEFAULT_KEY);
//如果沒(méi)有,就新建一個(gè)鹃锈,然后設(shè)置回緩存中
if(instance==null){
instance = new Singleton();
map.put(DEFAULT_KEY, instance);
}
//如果有就直接使用
return instance;
}
}
3.7 單例模式的優(yōu)缺點(diǎn)##
- 時(shí)間和空間
懶漢式是典型的時(shí)間換空間奥帘,也就是每次獲取實(shí)例都會(huì)進(jìn)行判斷,看是否需要?jiǎng)?chuàng)建實(shí)例仪召,費(fèi)判斷的時(shí)間寨蹋,當(dāng)然,如果一直沒(méi)有人使用的話扔茅,那就不會(huì)創(chuàng)建實(shí)例已旧,節(jié)約內(nèi)存空間。
餓漢式是典型的空間換時(shí)間召娜,當(dāng)類裝載的時(shí)候就會(huì)創(chuàng)建類實(shí)例运褪,不管你用不用,先創(chuàng)建出來(lái)玖瘸,然后每次調(diào)用的時(shí)候秸讹,就不需要再判斷了,節(jié)省了運(yùn)行時(shí)間雅倒。
- 線程安全
(1)從線程安全性上講璃诀,不加同步的懶漢式是線程不安全的,比如說(shuō):有兩個(gè)線程蔑匣,一個(gè)是線程A劣欢,一個(gè)是線程B,它們同時(shí)調(diào)用getInstance方法裁良,那就可能導(dǎo)致并發(fā)問(wèn)題凿将。如下示例:
程序繼續(xù)運(yùn)行,兩個(gè)線程都向前走了一步价脾,如下:
可能有些朋友會(huì)覺(jué)得文字描述還是不夠直觀牧抵,再來(lái)畫個(gè)圖說(shuō)明一下,如圖所示:
通過(guò)上圖的分解描述侨把,明顯可以看出犀变,當(dāng)A、B線程并發(fā)的情況下座硕,會(huì)創(chuàng)建出兩個(gè)實(shí)例來(lái)弛作,也就是單例的控制在并發(fā)情況下失效了。
(2)餓漢式是線程安全的华匾,因?yàn)樘摂M機(jī)保證了只會(huì)裝載一次映琳,在裝載類的時(shí)候是不會(huì)發(fā)生并發(fā)的。
(3)如何實(shí)現(xiàn)懶漢式的線程安全呢蜘拉?當(dāng)然懶漢式也是可以實(shí)現(xiàn)線程安全的萨西,只要加上synchronized即可,如下:
public static synchronized Singleton getInstance(){}
但是這樣一來(lái)旭旭,會(huì)降低整個(gè)訪問(wèn)的速度谎脯,而且每次都要判斷,也確實(shí)是稍微慢點(diǎn)持寄。那么有沒(méi)有更好的方式來(lái)實(shí)現(xiàn)呢源梭?
(4)雙重檢查加鎖娱俺,可以使用“雙重檢查加鎖”的方式來(lái)實(shí)現(xiàn),就可以既實(shí)現(xiàn)線程安全废麻,又能夠使性能不受到大的影響荠卷。那么什么是“雙重檢查加鎖”機(jī)制呢?
所謂雙重檢查加鎖機(jī)制烛愧,指的是:并不是每次進(jìn)入getInstance方法都需要同步油宜,而是先不同步,進(jìn)入方法過(guò)后怜姿,先檢查實(shí)例是否存在慎冤,如果不存在才進(jìn)入下面的同步塊,這是第一重檢查沧卢。進(jìn)入同步塊過(guò)后蚁堤,再次檢查實(shí)例是否存在,如果不存在搏恤,就在同步的情況下創(chuàng)建一個(gè)實(shí)例违寿,這是第二重檢查。這樣一來(lái)熟空,就只需要同步一次了藤巢,從而減少了多次在同步情況下進(jìn)行判斷所浪費(fèi)的時(shí)間。
雙重檢查加鎖機(jī)制的實(shí)現(xiàn)會(huì)使用一個(gè)關(guān)鍵字volatile息罗,它的意思是:
被volatile修飾的變量的值掂咒,將不會(huì)被本地線程緩存,所有對(duì)該變量的讀寫都是直接操作共享內(nèi)存迈喉,從而確保多個(gè)線程能正確的處理該變量绍刮。
注意:在Java1.4及以前版本中,很多JVM對(duì)于volatile關(guān)鍵字的實(shí)現(xiàn)有問(wèn)題挨摸,會(huì)導(dǎo)致雙重檢查加鎖的失敗孩革,因此雙重檢查加鎖的機(jī)制只能用在Java5及以上的版本。
看看代碼可能會(huì)更清楚些得运,示例代碼如下:
public class Singleton {
/**
* 對(duì)保存實(shí)例的變量添加volatile的修飾
*/
private volatile static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
//先檢查實(shí)例是否存在膝蜈,如果不存在才進(jìn)入下面的同步塊
if(instance == null){
//同步塊,線程安全的創(chuàng)建實(shí)例
synchronized(Singleton.class){
//再次檢查實(shí)例是否存在熔掺,如果不存在才真的創(chuàng)建實(shí)例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
這種實(shí)現(xiàn)方式既可使實(shí)現(xiàn)線程安全的創(chuàng)建實(shí)例饱搏,又不會(huì)對(duì)性能造成太大的影響,它只是在第一次創(chuàng)建實(shí)例的時(shí)候同步置逻,以后就不需要同步了推沸,從而加快運(yùn)行速度。
提示:
由于volatile關(guān)鍵字可能會(huì)屏蔽掉虛擬機(jī)中一些必要的代碼優(yōu)化,所以運(yùn)行效率并不是很高鬓催,因此一般建議肺素,沒(méi)有特別的需要,不要使用深浮。也就是說(shuō)压怠,雖然可以使用雙重加鎖機(jī)制來(lái)實(shí)現(xiàn)線程安全的單例,但并不建議大量采用飞苇,根據(jù)情況來(lái)選用吧。
3.8 在Java中一種更好的單例實(shí)現(xiàn)方式##
根據(jù)上面的分析蜗顽,常見(jiàn)的兩種單例實(shí)現(xiàn)方式都存在小小的缺陷布卡,那么有沒(méi)有一種方案,既能夠?qū)崿F(xiàn)延遲加載雇盖,又能夠?qū)崿F(xiàn)線程安全呢忿等?
還真有高人想到這樣的解決方案了,這個(gè)解決方案被稱為L(zhǎng)azy initialization holder class模式崔挖,這個(gè)模式綜合使用了Java的類級(jí)內(nèi)部類和多線程缺省同步鎖的知識(shí)贸街,很巧妙的同時(shí)實(shí)現(xiàn)了延遲加載和線程安全。
- 先來(lái)看點(diǎn)相應(yīng)的基礎(chǔ)知識(shí)
什么是類級(jí)內(nèi)部類狸相?簡(jiǎn)單點(diǎn)說(shuō)薛匪,類級(jí)內(nèi)部類指的是:有static修飾的成員式內(nèi)部類。如果沒(méi)有static修飾的成員式內(nèi)部類被稱為對(duì)象級(jí)內(nèi)部類脓鹃。
類級(jí)內(nèi)部類相當(dāng)于其外部類的static成分逸尖,它的對(duì)象與外部類對(duì)象間不存在依賴關(guān)系,因此可直接創(chuàng)建瘸右。而對(duì)象級(jí)內(nèi)部類的實(shí)例娇跟,是綁定在外部對(duì)象實(shí)例中的。
類級(jí)內(nèi)部類中太颤,可以定義靜態(tài)的方法苞俘,在靜態(tài)方法中只能夠引用外部類中的靜態(tài)成員方法或者成員變量。
類級(jí)內(nèi)部類相當(dāng)于其外部類的成員龄章,只有在第一次被使用的時(shí)候才會(huì)被裝載吃谣。
再來(lái)看看多線程缺省同步鎖的知識(shí)。
大家都知道瓦堵,在多線程開發(fā)中基协,為了解決并發(fā)問(wèn)題,主要是通過(guò)使用synchronized來(lái)加互斥鎖進(jìn)行同步控制菇用。
但是在某些情況中澜驮,JVM已經(jīng)隱含地為您執(zhí)行了同步,這些情況下就不用自己再來(lái)進(jìn)行同步控制了惋鸥。這些情況包括:
由靜態(tài)初始化器(在靜態(tài)字段上或 static{} 塊中的初始化器)初始化數(shù)據(jù)時(shí)
訪問(wèn) final 字段時(shí)
在創(chuàng)建線程之前創(chuàng)建對(duì)象時(shí)
線程可以看見(jiàn)它將要處理的對(duì)象時(shí)
- 接下來(lái)看看這種解決方案的思路
要想很簡(jiǎn)單的實(shí)現(xiàn)線程安全杂穷,可以采用靜態(tài)初始化器的方式悍缠,它可以由JVM來(lái)保證線程安全性。比如前面的“餓漢式”實(shí)現(xiàn)方式耐量,但是這樣一來(lái)飞蚓,不是會(huì)浪費(fèi)一定的空間嗎?因?yàn)檫@種實(shí)現(xiàn)方式廊蜒,會(huì)在類裝載的時(shí)候就初始化對(duì)象趴拧,不管你需不需要。
如果現(xiàn)在有一種方法能夠讓類裝載的時(shí)候不去初始化對(duì)象山叮,那不就解決問(wèn)題了著榴?一種可行的方式就是采用類級(jí)內(nèi)部類,在這個(gè)類級(jí)內(nèi)部類里面去創(chuàng)建對(duì)象實(shí)例屁倔,這樣一來(lái)脑又,只要不使用到這個(gè)類級(jí)內(nèi)部類,那就不會(huì)創(chuàng)建對(duì)象實(shí)例锐借。從而同時(shí)實(shí)現(xiàn)延遲加載和線程安全问麸。
看看代碼示例可能會(huì)更清晰,示例代碼如下:
public class Singleton {
/**
* 類級(jí)的內(nèi)部類钞翔,也就是靜態(tài)的成員式內(nèi)部類严卖,該內(nèi)部類的實(shí)例與外部類的實(shí)例
* 沒(méi)有綁定關(guān)系,而且只有被調(diào)用到才會(huì)裝載嗅战,從而實(shí)現(xiàn)了延遲加載
*/
private static class SingletonHolder {
/**
* 靜態(tài)初始化器妄田,由JVM來(lái)保證線程安全
*/
private static Singleton instance = new Singleton();
}
/**
* 私有化構(gòu)造方法
*/
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
仔細(xì)想想,是不是很巧妙呢驮捍!
當(dāng)getInstance方法第一次被調(diào)用的時(shí)候疟呐,它第一次讀取SingletonHolder.instance,導(dǎo)致SingletonHolder類得到初始化东且;而這個(gè)類在裝載并被初始化的時(shí)候启具,會(huì)初始化它的靜態(tài)域,從而創(chuàng)建Singleton的實(shí)例珊泳,由于是靜態(tài)的域鲁冯,因此只會(huì)被虛擬機(jī)在裝載類的時(shí)候初始化一次,并由虛擬機(jī)來(lái)保證它的線程安全性色查。
這個(gè)模式的優(yōu)勢(shì)在于薯演,getInstance方法并沒(méi)有被同步,并且只是執(zhí)行一個(gè)域的訪問(wèn)秧了,因此延遲初始化并沒(méi)有增加任何訪問(wèn)成本跨扮。
3.9 單例和枚舉##
按照《高效Java 第二版》中的說(shuō)法:單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法。
為了理解這個(gè)觀點(diǎn),先來(lái)了解一點(diǎn)相關(guān)的枚舉知識(shí)衡创,這里只是強(qiáng)化和總結(jié)一下枚舉的一些重要觀點(diǎn)帝嗡,更多基本的枚舉的使用,請(qǐng)參看Java編程入門資料:
Java的枚舉類型實(shí)質(zhì)上是功能齊全的類璃氢,因此可以有自己的屬性和方法哟玷;
Java枚舉類型的基本思想:通過(guò)公有的靜態(tài)final域?yàn)槊總€(gè)枚舉常量導(dǎo)出實(shí)例的類;
從某個(gè)角度講一也,枚舉是單例的泛型化巢寡,本質(zhì)上是單元素的枚舉;
用枚舉來(lái)實(shí)現(xiàn)單例非常簡(jiǎn)單塘秦,只需要編寫一個(gè)包含單個(gè)元素的枚舉類型即可讼渊,示例代碼如下:
/**
* 使用枚舉來(lái)實(shí)現(xiàn)單例模式的示例
*/
public enum Singleton {
/**
* 定義一個(gè)枚舉的元素,它就代表了Singleton的一個(gè)實(shí)例
*/
uniqueInstance;
/**
* 示意方法,單例可以有自己的操作
*/
public void singletonOperation(){
//功能處理
}
}
使用枚舉來(lái)實(shí)現(xiàn)單實(shí)例控制尊剔,會(huì)更加簡(jiǎn)潔,而且無(wú)償?shù)奶峁┝诵蛄谢臋C(jī)制菱皆,并由JVM從根本上提供保障须误,絕對(duì)防止多次實(shí)例化,是更簡(jiǎn)潔仇轻、高效京痢、安全的實(shí)現(xiàn)單例的方式。
3.10 思考單例模式##
- 單例模式的本質(zhì)
單例模式的本質(zhì):控制實(shí)例數(shù)目篷店。
單例模式是為了控制在運(yùn)行期間祭椰,某些類的實(shí)例數(shù)目只能有一個(gè)∑I拢可能有人就會(huì)想了方淤,那么我能不能控制實(shí)例數(shù)目為2個(gè),3個(gè)蹄殃,或者是任意多個(gè)呢携茂?目的都是一樣的,節(jié)省資源啊诅岩,有些時(shí)候單個(gè)實(shí)例不能滿足實(shí)際的需要讳苦,會(huì)忙不過(guò)來(lái),根據(jù)測(cè)算吩谦,3個(gè)實(shí)例剛剛好鸳谜,也就是說(shuō),現(xiàn)在要控制實(shí)例數(shù)目為3個(gè)式廷,怎么辦呢咐扭?
其實(shí)思路很簡(jiǎn)單,就是利用上面通過(guò)Map來(lái)緩存實(shí)現(xiàn)單例的示例,進(jìn)行變形草描,一個(gè)Map可以緩存任意多個(gè)實(shí)例览绿,新的問(wèn)題就是,Map中有多個(gè)實(shí)例穗慕,但是客戶端調(diào)用的時(shí)候饿敲,到底返回那一個(gè)實(shí)例呢?逛绵,也就是實(shí)例的調(diào)度問(wèn)題怀各,我們只是想要來(lái)展示設(shè)計(jì)模式,對(duì)于這個(gè)調(diào)度算法就不去深究了术浪,做個(gè)最簡(jiǎn)單的瓢对,循環(huán)返回就好了,示例代碼如下:
/**
* 簡(jiǎn)單演示如何擴(kuò)展單例模式胰苏,控制實(shí)例數(shù)目為3個(gè)
*/
public class OneExtend {
/**
* 定義一個(gè)缺省的key值的前綴
*/
private final static String DEFAULT_PREKEY = "Cache";
/**
* 緩存實(shí)例的容器
*/
private static Map<String,OneExtend> map = new HashMap<String,OneExtend>();
/**
* 用來(lái)記錄當(dāng)前正在使用第幾個(gè)實(shí)例硕蛹,到了控制的最大數(shù)目,就返回從1開始
*/
private static int num = 1;
/**
* 定義控制實(shí)例的最大數(shù)目
*/
private final static int NUM_MAX = 3;
private OneExtend(){}
public static OneExtend getInstance(){
String key = DEFAULT_PREKEY+num;
//緩存的體現(xiàn)硕并,通過(guò)控制緩存的數(shù)據(jù)多少來(lái)控制實(shí)例數(shù)目
OneExtend oneExtend = map.get(key);
if(oneExtend==null){
oneExtend = new OneExtend();
map.put(key, oneExtend);
}
//把當(dāng)前實(shí)例的序號(hào)加1
num++;
if(num > NUM_MAX){
//如果實(shí)例的序號(hào)已經(jīng)達(dá)到最大數(shù)目了法焰,那就重復(fù)從1開始獲取
num = 1;
}
return oneExtend;
}
public static void main(String[] args) {
//測(cè)試是否能滿足功能要求
OneExtend t1 = getInstance ();
OneExtend t2 = getInstance ();
OneExtend t3 = getInstance ();
OneExtend t4 = getInstance ();
OneExtend t5 = getInstance ();
OneExtend t6 = getInstance ();
System.out.println("t1=="+t1);
System.out.println("t2=="+t2);
System.out.println("t3=="+t3);
System.out.println("t4=="+t4);
System.out.println("t5=="+t5);
System.out.println("t6=="+t6);
}
}
測(cè)試一下,看看結(jié)果倔毙,如下:
t1==cn.javass.dp.singleton.example9.OneExtend@6b97fd
t2==cn.javass.dp.singleton.example9.OneExtend@1c78e57
t3==cn.javass.dp.singleton.example9.OneExtend@5224ee
t4==cn.javass.dp.singleton.example9.OneExtend@6b97fd
t5==cn.javass.dp.singleton.example9.OneExtend@1c78e57
t6==cn.javass.dp.singleton.example9.OneExtend@5224ee
- 何時(shí)選用單例模式
建議在如下情況中埃仪,選用單例模式:
當(dāng)需要控制一個(gè)類的實(shí)例只能有一個(gè),而且客戶只能從一個(gè)全局訪問(wèn)點(diǎn)訪問(wèn)它時(shí)陕赃,可以選用單例模式卵蛉,這些功能恰好是單例模式要解決的問(wèn)題。