摘要:構(gòu)造函數(shù)被調(diào)用或者,我們利用初始化塊,在初始化的時(shí)候就完成實(shí)例化構(gòu)造器被調(diào)用雙重檢查鎖定避免懶漢模式造成性能低下的另一個(gè)思路就是雙重檢查鎖定。
1. 什么是單例
保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)訪問它的全局訪問點(diǎn)。適用于:
當(dāng)類只能有一個(gè)實(shí)例而且客戶可以從一個(gè)眾所周知的訪問點(diǎn)訪問它時(shí)。
當(dāng)這個(gè)唯一實(shí)例應(yīng)該是通過子類化可擴(kuò)展的,并且客戶應(yīng)該無需更改代碼就能使用一個(gè)擴(kuò)展的實(shí)例時(shí)。
在單例模式中,有下列參與者:
Singleton:
定義一個(gè)Instance操作,允許客戶訪問它的唯一實(shí)例。Instance是一個(gè)類操作。
可能負(fù)責(zé)創(chuàng)建它自己的唯一實(shí)例。
2. 不考慮多線程的情況下的單例下面就是一個(gè)單例的實(shí)現(xiàn):Singleton0(當(dāng)然,這個(gè)示例在多線程下有問題)
// 單例程序: Singleton0 class Printer{ private static Printer printer; private Printer(){ } public static Printer getInstance(){ if(printer == null){ printer = new Printer(); } return printer; } } public class Singleton { public static void main(String[] args) { Printer p1 = Printer.getInstance(); Printer p2 = Printer.getInstance(); System.out.println(p1); System.out.println(p2); } } /* 運(yùn)行結(jié)果: * Printer@659e0bfd * Printer@659e0bfd * 完全一樣,表示只創(chuàng)建了Printer對(duì)象的一個(gè)實(shí)例 */3. 多線程環(huán)境下的單例
很可惜,上面的單例程序在多線程環(huán)境下,會(huì)華麗麗的出錯(cuò)!
3.1. 上述單例在多線程環(huán)境下的問題我們修改一下Singleton0,成為如下形式:Singleton1
public class Singleton1 { private static Singleton1 s1; private Singleton1(){ System.out.println("構(gòu)造函數(shù)被調(diào)用!"); } public static Singleton1 getInstance(){ if(s1 == null){ s1 = new Singleton1(); } return s1; } } // 測試程序,采用JUnit 4.x來測試 import org.junit.Test; public class Singleton1Test implements Runnable{ @Override public void run() { Singleton1.getInstance(); } @Test public void test() { for (int i = 0; i < 100000; i++) { Thread t = new Thread(new Singleton1Test(), "AnyThreadName"); t.start(); } } } /* 運(yùn)行結(jié)果:(可以發(fā)現(xiàn),構(gòu)造函數(shù)被多次調(diào)用!說明無法保證單例) * 構(gòu)造函數(shù)被調(diào)用! * 構(gòu)造函數(shù)被調(diào)用! * 構(gòu)造函數(shù)被調(diào)用! * 構(gòu)造函數(shù)被調(diào)用! * 構(gòu)造函數(shù)被調(diào)用! */
原因很簡單,在多線程的情況下,調(diào)用 Singleton1.getInstance() 的時(shí)候,可能會(huì)多個(gè)線程同時(shí)調(diào)用到,這個(gè)時(shí)候構(gòu)造函數(shù) Singleton1() 還沒有把 s1 實(shí)例化出來。這個(gè)時(shí)候判斷 s1 == null 是對(duì)的,所以多個(gè)線程都會(huì)去執(zhí)行:
if(s1 == null){ s1 = new Singleton1(); }
所以,這個(gè)構(gòu)造函數(shù)就會(huì)被執(zhí)行多次!
知道了這個(gè)原因,我們就可以很方便的找到解決方法,那就是:懶漢模式。
3.2. 懶漢模式既然是問題出在 getInstance() 上,那么我們就把這個(gè)方法設(shè)置成synchronized,這樣就可以保證同步了,于是我們修改成 Singleton2:
/** * 這種寫法能夠在多線程中很好的工作,而且看起來它也具備很好的lazy loading。 * 但是,遺憾的是,效率很低,99%情況下不需要同步。 * * @author martin.wang * */ public class Singleton2 { private static Singleton2 s2; private Singleton2(){ System.out.println("構(gòu)造函數(shù)被調(diào)用!"); } public static synchronized Singleton2 getInstance(){ if(s2 == null){ s2 = new Singleton2(); } return s2; } }
這個(gè)方式的最大問題就是:效率太低了。我們知道 synchronized 關(guān)鍵字很消耗資源,而且99%以上的可能性是不用 synchronized 的。每次都要 synchronized 有必要嗎?那么我們就有了2種解決方案:
餓漢模式。干脆一開始就給你初始化算了。
雙重檢查鎖定。只在必要的時(shí)候用 synchronized。
3.3. 餓漢模式餓漢模式避免了在 getInstance 的時(shí)候的判斷,所以效率高一點(diǎn)。不過也不是無懈可擊,如果這個(gè)構(gòu)造的過程很消費(fèi)時(shí)間,那么每次classloader的時(shí)間會(huì)非常長,沒有起到 lazyload 的效果。
public class Singleton3{ private static Singleton3 singleton3 = new Singleton3(); private Singleton3() { System.out.println("構(gòu)造函數(shù)被調(diào)用!"); } public static Singleton3 getInstance() { return singleton3; } }
或者,我們利用初始化塊,在初始化的時(shí)候就完成實(shí)例化
public class Singleton4 { private static Singleton4 s4 = null; static { s4 = new Singleton4(); } private Singleton4(){ System.out.println("構(gòu)造器被調(diào)用"); } public static Singleton4 getInstance(){ return s4; } }3.4. 雙重檢查鎖定
避免懶漢模式造成性能低下的另一個(gè)思路就是:雙重檢查鎖定。原理就是:
在 Singleton1 示例中造成問題的原因是 getInstance() 不同步
在 Singleton2 示例中造成性能低下的原因是不管三七二十一全同步
那么,我們就只檢查可能存在 同步問題 的代碼,讓代碼只在 可能存在問題的時(shí)候 再去做同步。
3.4.1. 雙重檢查鎖定的“實(shí)現(xiàn)”(有問題的!)public class Singleton7 { private static Singleton7 s7; private Singleton7() { System.out.println("構(gòu)造函數(shù)被調(diào)用"); } public static Singleton7 getInstance() { if(s7 == null) { synchronized (Singleton7.class) { // A if(s7 == null) { // B s7 = new Singleton7(); // C } } } return s7; } }
思路分析:
如果 s7 == null ,那么這個(gè)時(shí)候要同步了。
在注釋 A 的里面,設(shè)置一個(gè)同步鎖
如果線程 T1 訪問同步塊 A 中的代碼的時(shí)候,線程 T2 在 A 附近等待釋放鎖。
等 T1 線程完成,這個(gè)時(shí)候 T2 線程開始運(yùn)行 A 中的代碼。這個(gè)時(shí)候 s7 已經(jīng)被 T1 線程初始化了,執(zhí)行 B 的時(shí)候會(huì)返回 false,不會(huì)去執(zhí)行構(gòu)造函數(shù)。
不過,這個(gè)辦法還是不能保證完美無缺,還存在至少是理論上的缺陷。
3.4.2. 原因分析雙重檢查鎖定背后的理論是完美的。不幸地是,現(xiàn)實(shí)完全不同。雙重檢查鎖定的問題是:并不能保證它會(huì)在單處理器或多處理器計(jì)算機(jī)上順利運(yùn)行。
雙重檢查鎖定失敗的問題并不歸咎于 JVM 中的實(shí)現(xiàn) bug,而是歸咎于 Java 平臺(tái)內(nèi)存模型。內(nèi)存模型允許所謂的“無序?qū)懭搿薄?/p>
也就是說,不能保證 A, B, C 是按順序運(yùn)行的,這個(gè)可以Google一下 指令重排,這里不展開了。
說實(shí)話,我自己測試了100+次,并沒有出現(xiàn)構(gòu)造函數(shù)出現(xiàn)2次或以上的情況。出現(xiàn)這情況的概率很小很小。Java的內(nèi)存模型很復(fù)雜,牽涉到具體的JVM實(shí)現(xiàn)。3.4.3. 修改后的加強(qiáng)版
public class Singleton7 { private static volatile Singleton7 s7; private Singleton7() { System.out.println("構(gòu)造函數(shù)被調(diào)用"); } public static Singleton7 getInstance() { if(s7 == null) { synchronized (Singleton7.class) { // A if(s7 == null) { // B s7 = new Singleton7(); // C } } } return s7; } }
不過,據(jù)說這個(gè)也不是特別靠譜,我不去深究了。
看到這里,你是否有種想罵人的沖動(dòng)?什么鬼,做個(gè)單例模式就這么難啊。有沒有更方便的辦法?有,還不止一種:
3.5. 靜態(tài)內(nèi)部類法public class Singleton5 { private static class SingletonHolder{ private static final Singleton5 INSTANCE = new Singleton5(); } private Singleton5() { System.out.println("構(gòu)造函數(shù)被調(diào)用"); } public static final Singleton5 getInstance() { return SingletonHolder.INSTANCE; } } // 測試方法: import org.junit.Test; public class Singleton5Test implements Runnable{ @Override public void run() { Singleton5.getInstance(); } @Test public void test() { for (int i = 0; i < 10000; i++) { Thread t = new Thread(new Singleton4Test(), "T5"); t.start(); } } }3.6. 枚舉類法
枚舉類發(fā)是《Effective Java》的作者 Josh Bloch 推薦的一種實(shí)現(xiàn)方式,除了具有上述方法的優(yōu)點(diǎn)的話,還能防止反序列化重新創(chuàng)建新的對(duì)象、防止被反射攻擊。超級(jí)牛叉!
TODO:現(xiàn)在還沒有去寫利用枚舉類法防止反序列化,反射攻擊的測試用例。希望以后來填坑。
public enum Singleton6 { INSTANCE; private Singleton6() { System.out.println("構(gòu)造函數(shù)被調(diào)用"); } protected void doSomething() { } } // 測試: import org.junit.Test; public class Singleton6Test implements Runnable{ @Override public void run() { Singleton6.INSTANCE.doSomething(); } @Test public void test() { for (int i = 0; i < 1000; i++) { Thread t = new Thread(new Singleton6Test()); t.start(); } } }4. 總結(jié)
如果沒有什么特別需要,我個(gè)人認(rèn)為還是用餓漢方式算了,簡單有效。如果有懶加載要求,就用靜態(tài)內(nèi)部類法,也不錯(cuò)。有反序列化要求的,就用枚舉類法。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/68765.html
摘要:總結(jié)單例是運(yùn)用頻率很高的模式,因?yàn)榭蛻舳藳]有高并發(fā)的情況,選擇哪種方式并不會(huì)有太大的影響,出于效率考慮,推薦使用和靜態(tài)內(nèi)部類實(shí)現(xiàn)單例模式。 單例模式介紹 單例模式是應(yīng)用最廣的模式之一,也可能是很多人唯一會(huì)使用的設(shè)計(jì)模式。在應(yīng)用單例模式時(shí),單例對(duì)象的類必須保證只用一個(gè)實(shí)例存在。許多時(shí)候整個(gè)系統(tǒng)只需要一個(gè)全局對(duì)象,這樣有利于我么能協(xié)調(diào)整個(gè)系統(tǒng)整體的行為。 單例模式的使用場景 確保某個(gè)類有且...
摘要:不符合設(shè)計(jì)模式中的單一職責(zé)的概念。引入代理實(shí)現(xiàn)單例模式引入代理實(shí)現(xiàn)單例模式的特點(diǎn)我們負(fù)責(zé)管理單例的邏輯移到了代理類中。的單例模式對(duì)比在以上的代碼中實(shí)現(xiàn)的單例模式都混入了傳統(tǒng)面向?qū)ο笳Z言的特點(diǎn)。 聲明:這個(gè)系列為閱讀《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》 ----曾探@著一書的讀書筆記 1.單例模式的特點(diǎn)和定義 保證一個(gè)類僅有一個(gè)實(shí)例,并且提供一個(gè)訪問它的全局訪問點(diǎn)。 2.傳統(tǒng)面向?qū)?..
摘要:如果需要防范這種攻擊,請(qǐng)修改構(gòu)造函數(shù),使其在被要求創(chuàng)建第二個(gè)實(shí)例時(shí)拋出異常。單例模式與單一職責(zé)原則有沖突。源碼地址參考文獻(xiàn)設(shè)計(jì)模式之禪 定義 單例模式是一個(gè)比較簡單的模式,其定義如下: 保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)訪問它的全局訪問點(diǎn)。 或者 Ensure a class has only one instance, and provide a global point of ac...
摘要:在設(shè)計(jì)模式一書中,將單例模式稱作單件模式。通過關(guān)鍵字,來保證不會(huì)同時(shí)有兩個(gè)線程進(jìn)入該方法的實(shí)例對(duì)象改善多線程問題為了符合大多數(shù)程序,很明顯地,我們需要確保單例模式能在多線程的情況下正常工作。 在《Head First 設(shè)計(jì)模式》一書中,將單例模式稱作單件模式。這里為了適應(yīng)大環(huán)境,把它稱之為大家更熟悉的單例模式。 一、了解單例模式 1.1 什么是單例模式 單例模式確保一個(gè)類只有一個(gè)實(shí)例,...
閱讀 1437·2021-11-25 09:43
閱讀 2580·2021-09-24 10:30
閱讀 3659·2021-09-06 15:02
閱讀 3593·2019-08-30 15:55
閱讀 3300·2019-08-30 15:53
閱讀 1693·2019-08-30 15:52
閱讀 2142·2019-08-30 14:21
閱讀 2010·2019-08-30 13:55