Singleton Pattern 單例模式,作為創(chuàng)建型模式的一種,其保證了類的實(shí)例對(duì)象只有一個(gè),并對(duì)外提供此唯一實(shí)例的訪問(wèn)接口
成都創(chuàng)新互聯(lián)公司是一家專業(yè)提供貢井企業(yè)網(wǎng)站建設(shè),專注與成都做網(wǎng)站、成都網(wǎng)站建設(shè)、html5、小程序制作等業(yè)務(wù)。10年已為貢井眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)的建站公司優(yōu)惠進(jìn)行中。
對(duì)于單例模式而言,其最核心的目的就是為了保證該類的實(shí)例對(duì)象是唯一的。為此一方面,需要將該類的構(gòu)造函數(shù)設(shè)為private,另一方面,該類需要在內(nèi)部完成實(shí)例的構(gòu)造并對(duì)外提供訪問(wèn)接口。單例模式的好處顯而易見(jiàn),可以避免頻繁創(chuàng)建、銷毀實(shí)例所帶來(lái)的性能開(kāi)銷;但其缺點(diǎn)也同樣明顯,此類不僅需要描述業(yè)務(wù)邏輯,同時(shí)還需要構(gòu)造出該類的唯一對(duì)象并對(duì)外提供訪問(wèn)接口,其顯然違背了單一職責(zé)原則
單例模式的思想雖然簡(jiǎn)單易懂,但實(shí)現(xiàn)起來(lái)卻可謂是花樣繁多、妙不可言。這里來(lái)介紹幾種常見(jiàn)的單例模式的實(shí)現(xiàn)
如下實(shí)現(xiàn)最為簡(jiǎn)單,當(dāng) SingletonDemo1 類被加載到JVM中,即會(huì)完成實(shí)例化。即不是所謂的Lazy Load 延遲加載,故通常被稱之為 “餓漢式” 單例。其最大的問(wèn)題就在,可能構(gòu)造出來(lái)的實(shí)例對(duì)象從頭到尾沒(méi)有被使用過(guò)(沒(méi)有調(diào)用過(guò)getInstance方法),從而浪費(fèi)內(nèi)存。可能有人會(huì)對(duì)此有些困惑,SingletonDemo1 類被加載到JVM中了,那肯定是因?yàn)檎{(diào)用了getInstance方法啊。難道還有別的原因?答案是肯定的
這里,我們先簡(jiǎn)要補(bǔ)充一些類加載機(jī)制的相關(guān)知識(shí)點(diǎn)。我們知道Java中的類被加載到JVM中,通常會(huì)有如下幾個(gè)階段:加載、 驗(yàn)證、準(zhǔn)備、解析、初始化等。其中對(duì)于初始化階段而言,虛擬機(jī)規(guī)范嚴(yán)格規(guī)定了有且僅有以下5種情況必須立即對(duì)類進(jìn)行初始化(而加載、 驗(yàn)證、準(zhǔn)備顯然必須在此之前開(kāi)始):
說(shuō)到這里,大家可能就明白了,如果SingletonDemo1類中還有其他靜態(tài)方法,一旦被調(diào)用就會(huì)導(dǎo)致SingletonDemo1類被加載、初始化,此時(shí)即完成了實(shí)例的構(gòu)造。眾所周知,JVM保證了類加載過(guò)程的線程安全,所以餓漢式單例同樣是線程安全的
/**
* 單例模式1,餓漢式
*/
public class SingletonDemo1 {
private static SingletonDemo1 instance = new SingletonDemo1("我是餓漢式的單例");
private String description;
/**
* 私有構(gòu)造器
* @param description
*/
private SingletonDemo1(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
/**
* 提供實(shí)例的訪問(wèn)接口
* @return
*/
public static SingletonDemo1 getInstance() {
return instance;
}
public static void main(String[] args) {
SingletonDemo1 singletonDemo1 = SingletonDemo1.getInstance();
singletonDemo1.getInfo();
}
}
測(cè)試結(jié)果如下所示
前面說(shuō)到,餓漢式單例會(huì)導(dǎo)致內(nèi)存空間的浪費(fèi),那么有沒(méi)有辦法解決這個(gè)問(wèn)題呢?答案是有的,這就是”懶漢式”單例。顧名思義,其實(shí)例不是在類加載、初始化時(shí)被構(gòu)建的,而是在真正需要的時(shí)候才去創(chuàng)建,如下所示
/**
* 單例模式2,線程不安全的懶漢式
*/
public class SingletonDemo2 {
private static SingletonDemo2 instance = null;
private String description;
private SingletonDemo2(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
public static SingletonDemo2 getInstance() {
if( instance==null ) {
instance = new SingletonDemo2("我是線程不安全的懶漢式單例");
}
return instance;
}
public static void main(String[] args) {
SingletonDemo2 singletonDemo2 = SingletonDemo2.getInstance();
singletonDemo2.getInfo();
}
}
測(cè)試結(jié)果如下所示
“懶漢式”單例雖然實(shí)現(xiàn)了Lazy Load延遲加載,但是其存在一個(gè)很嚴(yán)重的問(wèn)題,不是線程安全的。所以如果在多線程環(huán)境下,我們需要使用下面線程安全的”懶漢式”單例,其保障線程安全的手段也很簡(jiǎn)單,直接使用synchronized來(lái)修飾getInstance方法。這種辦法過(guò)于簡(jiǎn)單粗暴,同時(shí)會(huì)導(dǎo)致效率十分低下。實(shí)例一旦被構(gòu)造完畢后,由于鎖的存在,導(dǎo)致每次只能由一個(gè)線程可以獲取到實(shí)例對(duì)象
/**
* 單例模式3, 線程安全但效率低下的懶漢式
*/
public class SingletonDemo3 {
private static SingletonDemo3 intance = null;
private String description;
private SingletonDemo3(String description) {
this.description = description;
}
public void getInfo() {
System.out.printf(description);
}
public static synchronized SingletonDemo3 getInstance() {
if( intance==null ) {
intance = new SingletonDemo3("我是線程安全線程安全但效率低下的懶漢式單例");
}
return intance;
}
public static void main(String[] args) {
SingletonDemo3 singletonDemo3 = SingletonDemo3.getInstance();
singletonDemo3.getInfo();
}
}
測(cè)試結(jié)果如下所示
通過(guò)前面我們看到,無(wú)論是餓漢式單例還是懶漢式單例,其都有明顯的缺點(diǎn)。那么有沒(méi)有一種完美的單例?既可以實(shí)現(xiàn)Lazy Load延遲加載,又可以在保證線程安全的前提下依然具備較高的效率呢。答案是肯定——基于DCL(Double-Checked Locking)雙重檢查鎖的單例。其實(shí)現(xiàn)如下,該單例實(shí)現(xiàn)中進(jìn)行了兩次檢查。第一次檢查時(shí)如果發(fā)現(xiàn)實(shí)例已經(jīng)構(gòu)造完畢了,則無(wú)需加鎖直接返回實(shí)例對(duì)象即可。其保證了實(shí)例在構(gòu)建完成后,其他多個(gè)線程可以同時(shí)快速獲取該實(shí)例。第二次檢查時(shí)則是為了避免重復(fù)構(gòu)造實(shí)例,因?yàn)樵谶€未構(gòu)造實(shí)例前,可能會(huì)有多個(gè)線程通過(guò)了第一次檢查,準(zhǔn)備加鎖來(lái)構(gòu)造實(shí)例。在DCL的單例實(shí)現(xiàn)中,尤其需要注意的一點(diǎn)是靜態(tài)變量instance必須要使用volatile進(jìn)行修飾。其原因在于volatile禁止了指令的重排序。這里就此問(wèn)題再作一些詳細(xì)的解釋說(shuō)明:在JDK1.5之前的Java內(nèi)存模型中,雖然不允許volatile變量之間進(jìn)行重排序,但卻允許普通變量與volatile變量之間的重排序。所以在JSR 133(JDK 1.5)中對(duì)volatile變量的內(nèi)存語(yǔ)義進(jìn)一步增強(qiáng),即限制了普通變量與volatile變量之間是否可以重排序的具體場(chǎng)景。這也是為什么在JDK 1.5之前無(wú)法通過(guò)DCL實(shí)現(xiàn)一個(gè)線程安全的單例模式
/**
* 單例模式4,基于DCL的線程安全的單例
*/
public class SingletonDemo4 {
// 此處必須要使用volatile修飾!
private static volatile SingletonDemo4 instance = null;
private String description;
private SingletonDemo4(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
public static SingletonDemo4 getInstance() {
if( instance==null ) { // 第一次檢查:如果實(shí)例已經(jīng)構(gòu)造完成則直接取,避免每次取之前需要獲取鎖
synchronized (SingletonDemo4.class) {
if(instance==null) { // 第二次檢查:避免構(gòu)造出多個(gè)實(shí)例
instance = new SingletonDemo4("我是基于DCL的線程安全的單例");
}
}
}
return instance;
}
public static void main(String[] args) {
SingletonDemo4 singletonDemo4 = SingletonDemo4.getInstance();
singletonDemo4.getInfo();
}
}
測(cè)試結(jié)果如下
前面我們說(shuō)到的第一種單例實(shí)現(xiàn),之所以被稱為餓漢式、非延遲加載。其原因就在于類的加載、初始化不能100%保證是因?yàn)檎{(diào)用getInstance方法引起的。而這里我們通過(guò)靜態(tài)內(nèi)部類的方式來(lái)實(shí)現(xiàn)一個(gè)延遲加載的單例,代碼如下所示。當(dāng)調(diào)用外部類SingletonDemo5的一些靜態(tài)方法(當(dāng)然getInstance方法除外),只會(huì)加載、初始化外部類SingletonDemo5,而不會(huì)去初始化靜態(tài)內(nèi)部類SingletonDemo5Holder。只有通過(guò)調(diào)用getInstance方法訪問(wèn)了靜態(tài)內(nèi)部類SingletonDemo5Holder的靜態(tài)變量instance,靜態(tài)內(nèi)部類SingletonDemo5Holder才會(huì)被加載、初始化,顯然此時(shí)實(shí)例才會(huì)被真正的構(gòu)造。所以對(duì)于基于靜態(tài)內(nèi)部類的單例實(shí)現(xiàn)而言,其之所以能保證Lazy Load延遲加載特性,是其因?yàn)橥ㄟ^(guò)SingletonDemo5Holder靜態(tài)內(nèi)部類100%保證了靜態(tài)內(nèi)部類被加載、初始化是因?yàn)檎{(diào)用外部類的getInstance方法而導(dǎo)致的。同樣地,該方式的單例也是滿足線程安全的,原因在餓漢式單例實(shí)現(xiàn)中已作解釋,此處就不再贅述
/**
* 單例模式5,靜態(tài)內(nèi)部類
*/
public class SingletonDemo5 {
private String description;
private SingletonDemo5(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
private static class SingletonDemo5Holder{
private static final SingletonDemo5 instance = new SingletonDemo5("我是基于靜態(tài)內(nèi)部類的線程安全的單例");
}
public static SingletonDemo5 getInstance() {
return SingletonDemo5Holder.instance;
}
public static void main(String[] args) {
SingletonDemo5 singletonDemo5 = SingletonDemo5.getInstance();
singletonDemo5.getInfo();
}
}
測(cè)試結(jié)果如下所示
對(duì)于Java的枚舉類型而言,其構(gòu)造器是且只能是private私有的。故其特別適合用于實(shí)現(xiàn)單例模式。下面即是一個(gè)基于枚舉的單例實(shí)現(xiàn),可以看到此種實(shí)現(xiàn)非常簡(jiǎn)潔優(yōu)雅。當(dāng)枚舉類進(jìn)行加載、初始化時(shí),即會(huì)完成實(shí)例的構(gòu)建,我們通過(guò)枚舉的特性保證了實(shí)例的唯一性,當(dāng)然其不是Lazy Load延遲加載的。與此同時(shí)根據(jù)類的加載機(jī)制我們可知其也是線程安全的(由JVM保證)
/**
* 單例模式6,枚舉法
*/
public enum SingletonDemo6 {
INSTANCE("我是枚舉法的單例");
private String description;
/**
* 枚舉的構(gòu)造器默認(rèn)訪問(wèn)權(quán)限是private, 當(dāng)然也只能是私有的
* @param description
*/
SingletonDemo6(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
}
...
/**
* 測(cè)試用例
*/
public class SingletonDemo6Test {
public static void main(String[] args) {
SingletonDemo6 singletonDemo6 = SingletonDemo6.INSTANCE;
singletonDemo6.getInfo();
}
}
測(cè)試結(jié)果如下