前言
本篇文章主要介紹的是設(shè)計模式中的單例模式的實現(xiàn)方式犀盟。
什么是設(shè)計模式
設(shè)計模式其實就是前輩們長時間的試驗和錯誤總結(jié)出來的蹈胡,針對軟件開發(fā)過程中面臨的一般問題的解決方案。
設(shè)計模式分類
根據(jù)其目的(模式是用來做什么的)可分為創(chuàng)建型自沧,結(jié)構(gòu)型和行為型三種:
? 創(chuàng)建型模式主要用于創(chuàng)建對象昆汹。
? 結(jié)構(gòu)型模式主要用于處理類或?qū)ο蟮慕M合明刷。
? 行為型模式主要用于描述對類或?qū)ο笤鯓咏换ズ驮鯓臃峙渎氊煛?/p>
單例模式
單例模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的最佳方式满粗。這種模式保證一個系統(tǒng)中的某個類只有一個能夠被外界訪問的實例辈末。
單例模式的使用場景
在程序中比較常用的是數(shù)據(jù)庫連接池、線程池、日志對象等等挤聘。
單例模式的實現(xiàn)
單例模式的實現(xiàn)有5種方式轰枝,分別是懶漢式、餓漢式组去、雙重鎖鞍陨、靜態(tài)內(nèi)部類、枚舉从隆。
1.懶漢式
這種方式是最基本的實現(xiàn)方式诚撵,但是不支持多線程,線程不安全键闺。
實現(xiàn):定義一個私有的構(gòu)造方法寿烟,定義一個該類靜態(tài)私有的變量,然后定義一個公共的靜態(tài)方法辛燥,在靜態(tài)方法內(nèi)對變量的值進行空判斷筛武,不為空直接返回,如果為空重新構(gòu)建并賦值給改該變量挎塌。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
測試:
public class SingletonTest {
public static void main(String[] args) {
System.out.println(Singleton.getInstance()==Singleton.getInstance());
}
}
這里輸出的是true徘六,可以看到兩次獲取的實例其實是同一個。
采用多線程方式:
public class SingletonTest {
public static void main(String[] args) {
new Thread(new SingletonThread()).start();
new Thread(new SingletonThread()).start();
}
}
class SingletonThread implements Runnable {
@Override
public void run() {
System.out.println(Singleton.getInstance().hashCode());
}
}
此時兩次輸出的哈希值時而相同時而不同榴都,出現(xiàn)線程不安全問題硕噩。
這種方式可以在靜態(tài)方法的方法聲明上加synchronized關(guān)鍵字來確保線程安全,但是效率低下缭贡。
2.餓漢式
這種方式?jīng)]有加鎖炉擅,所以效率會提高。雖然沒有加鎖阳惹,但是也是線程安全的谍失,這是因為它在類加載時就初始化了,而一個類在整個生命周期中只會被加載一次莹汤,因此該單例類只會創(chuàng)建一個實例快鱼。所以餓漢式天生就是線程安全的。但也正是因為它在類加載的時候就初始化了纲岭,會一直占用內(nèi)存抹竹,導(dǎo)致內(nèi)存浪費。
定義一個私有的構(gòu)造方法止潮,并將自身的實例對象設(shè)置為一個靜態(tài)私有屬性,然后通過公共的靜態(tài)方法調(diào)用返回實例窃判。
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
測試:和懶漢式的測試代碼一樣,普通方式獲取的兩個實例是同一個喇闸,輸出true袄琳,多線程方式獲取的哈希值是一樣的询件。
3.雙重鎖
定義一個私有構(gòu)造方法,通過volatile定義靜態(tài)私有變量唆樊,保證了該變量的可見性宛琅,然后定義一個共有的靜態(tài)方法,第一次對該對象實例化時與否判斷逗旁,不為空直接返回嘿辟,提升效率;然后使用synchronized 進行同步代碼塊片效,防止對象未初始化時红伦,在多線程訪問該對象在第一次創(chuàng)建后,再次重復(fù)的被創(chuàng)建堤舒;然后第二次對該對象實例化時與否判斷,如果未初始化哺呜,則初始化舌缤,否則直接返回該實例。
第一次判空是為了提升效率某残,只有第一次實例化的時候需要加鎖国撵,而不是每次請求都加鎖
第二次是為了進行同步,避免多線程問題玻墅。
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) { //#1
synchronized (Singleton.class){ //#2
if (singleton == null) { //#3
singleton = new Singleton(); //#4
System.out.println(Thread.currentThread().getName() + ": singleton is initalized...");//#5.1
} else {
System.out.println(Thread.currentThread().getName() + ": singleton is not null now...");//#5.2
}
}
}
return singleton;
}
}
雙重鎖這種方式實現(xiàn)單例的關(guān)鍵點在于兩次判空介牙、加鎖、以及volatile關(guān)鍵字澳厢,這里解釋一下volatile關(guān)鍵字在這種方式實現(xiàn)單例起到的作用环础。
volatile有兩個特性
可見性:證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值剩拢,這新值對其他線程來說是立即可見的线得。
有序性:禁止進行指令重排序。
假設(shè)不加volatile關(guān)鍵字徐伐,這段代碼可能輸出的是
thread1: uniqueInstance is initalized...
thread2: uniqueInstance is initalized...
過程分析:
1.thread1進入#1贯钩,獲取到singleton為空,此時thread1讓出CPU資源給thread2办素,thread1進入#1角雷,卻在#2外等待。
2.thread2進入#1性穿,獲取到singleton為空勺三,此時thread2讓出CPU資源給thread1,
thread2進入#1需曾,卻在#2外等待檩咱。
3.thread1會依次執(zhí)行#2揭措,#3,#4刻蚯,#5.1绊含,最終在thread2里面實例化了singleton。thread1執(zhí)行完畢讓出CPU資源給thread2炊汹。
4.thread2接著#1跑下去躬充,跑到#3的時候,由于#1里面拿到的singleton還是空(并沒有及時從thread1里面拿到最新的)讨便,所以thread2仍然會執(zhí)行#4充甚,#5.1
5.最后在thread1和thread2都實例化了singleton。
這樣的話霸褒,singleton實例化了兩次伴找。而volatile關(guān)鍵字修飾變量的作用就是讓第四步中thread2及時拿到thread1更新的的singleton,使它最后執(zhí)行#5.2,這里利用的就是可見性废菱。
volatile使singleton重排序被禁止技矮,所有的寫(write)操作都將發(fā)生在讀(read)操作之前。也就確保了thread1的實例化是發(fā)生在thread2第二次判空之前的殊轴。
當然衰倦,這只是一種假設(shè)的情況,沒有重現(xiàn)過旁理,太難模擬了樊零,但是確實存在。
4.靜態(tài)內(nèi)部類
這種方式也是利用了類加載機制孽文,只不過它不像餓漢式一樣驻襟,Singleton類被加載就實例化,這樣就沒有達到懶加載的效果芋哭。外部類加載時并不需要立即加載內(nèi)部類塑悼,內(nèi)部類不被加載則不去初始化instance,因此不占內(nèi)存楷掉。而靜態(tài)內(nèi)部類實現(xiàn)單例保證線程安全厢蒜,是由JVM決定的。
public class Singleton {
private Singleton() {}
private static class SingletonInner {
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
return SingletonInner.singleton;
}
}
5.枚舉
枚舉是JDK1.5之后的特性烹植。無償提供序列化機制斑鸦,防止多次實例化。
public enum Singleton {
INSTANCE;
}
測試:輸出的哈希值都是一樣的草雕。
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new SingletonThread()).start();
}
}
}
class SingletonThread implements Runnable {
@Override
public void run() {
System.out.println(Singleton.INSTANCE.hashCode());
}
}
PS:第2.3.4種測試代碼和第一種是一樣的巷屿。
總結(jié):
一般情況下使用餓漢式;如果要求實現(xiàn)懶加載墩虹,則使用靜態(tài)內(nèi)部類嘱巾;如果涉及到反序列化創(chuàng)建對象時憨琳,可以嘗試使用枚舉方式。
CSDN:https://blog.csdn.net/qq_27682773
簡書:http://www.reibang.com/u/e99381e6886e
博客園:https://www.cnblogs.com/lixianguo