單例設計模式總結

1爲什麼要用單例設計模式

        在我們開發過程中有時候對於某個類的實例只能有一個,也就是說該類只能被創建一次,別人想用只能用這個實例。這個在我們數據庫的操作就是單例的一個體現,不同線程只能拿到一個DBHelper對象

/**
     * 雙重校驗的  單例模式
     * @return
     */
    public static DBHelper getInstance() {

        if (dbHelper == null) {
            synchronized (DBHelper.class) {
                if (dbHelper == null) {
                    dbHelper = new DBHelper(SupportApplication.get());
                    MyLog.v(TAG, BUSINESS_NAME);
                }
            }
        }

        return dbHelper;

    }

幾種單例設計模式

懶漢式

第一種:這種在多線程下沒發工作,基本上不算是單例

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  

    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
} 

第二種:這種效率低下,99%情況下不需要同步。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}  

第三種:雙重校驗鎖

public class Singleton {

    private static Singleton instance = null;
    private String name;

    private Singleton() {
    }

    private Singleton(String name) {
        this.name = name;
    }

    public static Singleton getinstance(String name) {
        if (instance == null) {
            synchronized (Singleton.class) {//1
                if (instance == null) {//2
                    instance = new Singleton(name);//3
                }
            }
        }
        return instance;
    }
}

雙重校驗鎖並不能保證安全

其實大家看到這個雙重校驗已經覺得是線程安全的了,很長一段時間我也這麼認爲,其實並不是,聽我慢慢道來:
雙重校驗鎖的失敗並不歸咎於jvm的bug,而是歸咎於java的內存模型,內存模型允許無序寫入。http://blog.csdn.net/suifeng3051/article/details/52611310
那什麼是無序寫入?
這篇博客很好的解釋了這個問題:http://www.cnblogs.com/dolphin0520/p/3920373.html


無序寫入:
爲解釋該問題,需要重新考察上述清單中的 //3 行。此行代碼創建了一個 Singleton 對象並初始化變量 instance 來引用此對象。這行代碼的問題是:在 Singleton 構造函數體執行之前,變量 instance 可能成爲非 null 的,即賦值語句在對象實例化之前調用,此時別的線程得到的是一個還會初始化的對象,這樣會導致系統崩潰。
什麼?這一說法可能讓您始料未及,但事實確實如此。在解釋這個現象如何發生前,請先暫時接受這一事實,我們先來考察一下雙重檢查鎖定是如何被破壞的。假設代碼執行以下事件序列:

1、線程 1 進入 getInstance() 方法。
2、由於 instance 爲 null,線程 1 在 //1 處進入 synchronized 塊。
3、線程 1 前進到 //3 處,但在構造函數執行之前,使實例成爲非 null。
4、線程 1 被線程 2 預佔。
5、線程 2 檢查實例是否爲 null。因爲實例不爲 null,線程 2 將 instance 引用返回給一個構造完整但部分初始化了的 Singleton 對象。
6、線程 2 被線程 1 預佔。
7、線程 1 通過運行 Singleton 對象的構造函數並將引用返回給它,來完成對該對象的初始化。

爲展示此事件的發生情況,假設代碼行 instance =new Singleton(); 執行了下列僞代碼:
mem = allocate(); //爲單例對象分配內存空間.
instance = mem; //注意,instance 引用現在是非空,但還未初始化
ctorSingleton(instance); //爲單例對象通過instance調用構造函數

這段僞代碼不僅是可能的,而且是一些 JIT 編譯器上真實發生的。執行的順序是顛倒的,但鑑於當前的內存模型,這也是允許發生的。JIT 編譯器的這一行爲使雙重檢查鎖定的問題只不過是一次學術實踐而已。

如果真像這篇文章:http://dev.csdn.net/author/axman/4c46d233b388419e9d8b025a3c507b17.html所說那樣的話,1.2或以後的版本就不會有問題了,但這個規則是JMM的規範嗎?誰能夠確認一下。
確實,在JAVA2(以jdk1.2開始)以前對於實例字段是直接在主儲區讀寫的.所以當一個線程對resource進行分配空間,
初始化和調用構造方法時,可能在其它線程中分配空間動作可見了,而初始化和調用構造方法還沒有完成.
但是從JAVA2以後,JMM發生了根本的改變,分配空間,初始化,調用構造方法只會在線程的工作存儲區完成,在沒有
向主存儲區複製賦值時,其它線程絕對不可能見到這個過程.而這個字段複製到主存區的過程,更不會有分配空間後
沒有初始化或沒有調用構造方法的可能.在JAVA中,一切都是按引用的值複製的.向主存儲區同步其實就是把線程工作
存儲區的這個已經構造好的對象有壓縮堆地址值COPY給主存儲區的那個變量.這個過程對於其它線程,要麼是resource
爲null,要麼是完整的對象.絕對不會把一個已經分配空間卻沒有構造好的對象讓其它線程可見.


什麼叫線程安全?
:一個類或者程序所提供的接口對於線程來說是原子操作,或者多個線程之間的切換不會導致該接口的執行結果存在二義性,也就是說我們不用考慮同步的問題,那就是線程安全的。
那麼對instance的操作是不是原子操作呢?
下面科普下什麼是原子操作


2.1.2,使用volelite關鍵詞

原子操作(atomic operation)是不需要synchronized”,這是Java多線程編程的老生常談了。所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切[1] 換到另一個線程)

Java中的原子操作包括:
1)除long和double之外的基本類型的賦值操作
2)所有引用reference的賦值操作
3)java.concurrent.Atomic.* 包中所有類的一切操作
count++不是原子操作,是3個原子操作組合
1.讀取主存中的count值,賦值給一個局部成員變量tmp
2.tmp+1
3.將tmp賦值給count
可能會出現線程1運行到第2步的時候,tmp值爲1;這時CPU調度切換到線程2執行完畢,count值爲1;切換到線程1,繼續執行第3步,count被賦值爲1————結果就是兩個線程執行完畢,count的值只加了1;
還有一點要注意,如果使用AtomicInteger.set(AtomicInteger.get() + 1),會和上述情況一樣有併發問題,要使用AtomicInteger.getAndIncrement()纔可以避免併發問題


繼續看這個例子
看起來INSTANCE = new SingleTon();是一條賦值語句,事實上,它並不是一個原子操作。它大概會做三件事情:
爲對象分配內存;
調用對應的構造做對象的初始化操作;
將引用INSTANCE指向新分配的空間。
這裏並沒有細化到指令的級別,但我們仍然可以分析出三個操作的依賴性: 2依賴於1,3依賴於1。第二步與第三步是獨立無依賴的,是可以被優化重排序的。
線程1:getInstance()
線程1:判斷INSTANCE是否爲空?Y
線程1:獲取同步鎖
線程1:判斷INSTANCE是否爲空? Y
線程1:爲新對象分配內存
線程1:將引用INSTANCE指向新分配的空間。
線程2:getInstance()
線程2:判斷INSTANCE是否爲空? N
線程2:返回INSTANCE對象 (擦。INSTANCE表示老子還沒被初始化呢)
線程2:使用INSTANCE對象時發現這貨不能用,bug found!
線程1:調用對應構造器作對象初始化操作。

我們說的是多線程環境下的執行,當然不會像上面那樣的線性過程,我想你懂我的意思的。這樣的bug不是一定會出現,卻是一個不小的隱患。

幸好,我們有volatile關鍵字提供內存屏障
大家對volatile關鍵字可能更多的印象是內存的可見性和提供的原子性。一個變量被聲明爲volatile後,在不同的線程的緩存中不會有副本,保證一致性。對聲明volatile的變量的任意讀都可以見到任意線程對這個volatile變量的寫入。對於加有volatile的變量,可以保證對它讀寫的原子性。
有的文章說volalite是不能保證原子操作的,但這又說具備原子性,那是不是矛盾呢,後來看了很多文章都沒有說清楚。
https://www.zhihu.com/question/35268028
https://conndots.github.io/2015/08/13/singleton-bug-2-jmm/
http://www.cnblogs.com/hapjin/p/5492880.html
直到看這個文章http://www.infoq.com/cn/articles/java-memory-model-4

他提到一個概念簡而言之,volatile變量自身具有下列特性:

可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。
原子性:對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性。

對於可見性,Java提供了volatile關鍵字來保證可見性。

  當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。

  而普通的共享變量不能保證可見性,因爲普通共享變量被修改之後,什麼時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。

  另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
這裏寫圖片描述

public class Singleton {

    private static volatile Singleton instance = null;
    private String name;

    private Singleton() {
    }

    private Singleton(String name) {
        this.name = name;
    }

    public static Singleton getinstance(String name) {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(name);
                }
            }
        }
        return instance;
    }
}

2.1.3,ThreadLocal修復雙重檢測

藉助於ThreadLocal,將臨界資源(需要同步的資源)線程局部化,具體到本例就是將雙重檢測的第一層檢測條件 if (instance == null) 轉換爲了線程局部範圍內來作。這裏的ThreadLocal也只是用作標示而已,用來標示每個線程是否已訪問過,如果訪問過,則不再需要走同步塊,這樣就提高了一定的效率。但是ThreadLocal在1.4以前的版本都較慢,但這與volatile相比卻是安全的。

public class Singleton {  
 private static final ThreadLocal perThreadInstance = new ThreadLocal();  
 private static Singleton singleton ;  
 private Singleton() {}  

 public static Singleton  getInstance() {  
  if (perThreadInstance.get() == null){  
   // 每個線程第一次都會調用  
   createInstance();  
  }  
  return singleton;  
 }  

 private static  final void createInstance() {  
  synchronized (Singleton.class) {  
   if (singleton == null){  
    singleton = new Singleton();  
   }  
  }  
  perThreadInstance.set(perThreadInstance);  
 }  
}  

爲了做到真真的延遲加載,雙重檢測在Java中是行不通的,所以只能藉助於另一類的類加載加延遲加載,下面看餓漢式;

2.2最安全的單例-餓漢式

餓漢式:隨着類的加載的時候就被實例話了,天生的具備了線程安全,既實現了線程安全,又避免了同步帶來的性能影響。

public class SingleTon2 {
    private static SingleTon2 instance = new SingleTon2();

    private SingleTon2() {

    }

    public static SingleTon2 getInstance() {
        return instance;
    }
}

3枚舉單例:

:這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,可謂是很堅強的壁壘啊,不過,個人認爲由於1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏,在實際工作中,我也很少看見有人這麼寫過。

public enum SingTon3 {
    INSTANCE;

    public void whateverMethod() {

    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章