一天一個設計模式——(Singleton)單例模式(線程安全性)

1、模式說明java

  有時候,咱們但願在應用程序中,僅生成某個類的一個實例,這時候須要用到單例模式。緩存

2、模式類圖安全

  

3、模式中的角色多線程

  •   Singleton角色,該模式中僅有的一個角色,該角色有一個返回惟一實例的getInstance方法,該方法老是返回同一個實例;

4、代碼示例併發

  單例模式比較簡單,要實現單例模式只需保證三點:ide

  • 私有化類的構造函數;
  • 在類中建立類的對象;
  • 提供獲取對象的公有方法;
package com.designpattern.cn.singletonpattern;

public class Singleton {
    private static Singleton singleton = new Singleton();

    //也可使用靜態域,在類加載時建立實例
    //static {singleton = new Singleton();}

    private Singleton(){
        System.out.println("Instance created!");
    }

    public static Singleton getInstance(){
        return singleton;
    }
}
View Code

 

測試類運行結果以下:函數

  上面的代碼中,Singleton類的方法和成員屬性都是靜態的,緣由是咱們不能直接建立Singleton類的實例,可是想經過類的方法調用獲取實例,所以方法必須是靜態的,同時,靜態方法只能操做靜態成員,因此對象也是靜態的。測試

  另外注意到,Singleton類的構造函數被設置爲私有的,這樣能夠避免經過調用new方法來直接建立對象。ui

 

5、擴展——幾種不一樣模式的單例模式實現及其線程安全性分析spa

  • 餓漢模式

  上面的代碼示例中實現的單例模式被稱爲「餓漢模式」,由於類在加載的過程當中,單例就已經初始化完成,確保在獲取Instance的時候,實例是已經存在的了。所以餓漢模式是線程安全的單例模式實現,能夠直接用於多線程環境。而且因爲是在類的加載階段就生成了實例,所以第一次調用獲取對象方法時效率高。缺點是:不管程序最終是否會用到這個類,這個類都會被建立,會佔用必定的內存。

  • 懶漢模式

  與餓漢模式相對的,若是咱們在加載類的時候不建立實例,等到第一次調用getInstance方法時才生成實例對象,這種作法就是「懶漢模式」。

懶漢模式相比於餓漢模式,因爲是在第一次調用getInstance方法時才經過構造函數建立對象,若是對象的建立代碼比較複雜,會影響第一次獲取對象的效率。同時,懶漢模式若是不作特殊處理,很明顯是線程不安全的,若是要使用懶漢模式又想用在多線程環境中,有幾種方式實現線程安全:

  一、第一個能想到的方法就是加鎖

package com.designpattern.cn.singletonpattern;

public class LazySingletonThreadLock {
    private static LazySingletonThreadLock lazySingletonThreadLock = null;
    private LazySingletonThreadLock(){};
    public static synchronized LazySingletonThreadLock getInstance(){
        if(lazySingletonThreadLock == null){
            lazySingletonThreadLock = new LazySingletonThreadLock();
        }
        return lazySingletonThreadLock;
    }
}
View Code

  這種寫法的最大缺點就是效率低!每一個線程在想得到類的實例調用getInstance()方法都要進行同步,實際上這個方法只執行一次實例化代碼就夠了,後面每次獲取實例直接return就好了,不用每次都讓方法同步。

二、第二個方法:雙重校驗鎖

package com.designpattern.cn.singletonpattern;

public class LazySingletonDoubleCheck {
    //雙重檢查方式
    private static volatile LazySingletonDoubleCheck lazySingletonDoubleCheck = null;
    private LazySingletonDoubleCheck(){}
    public static LazySingletonDoubleCheck getInstance(){
        if(lazySingletonDoubleCheck == null){
            synchronized (LazySingletonDoubleCheck.class){
                if(lazySingletonDoubleCheck == null){
                    lazySingletonDoubleCheck = new LazySingletonDoubleCheck();
                }
            }
        }
        return lazySingletonDoubleCheck;
    }
}
View Code

  這種寫法經過兩次判斷lazySingletonDoubleCheck成員是否爲空,從而決定是否建立實例,一旦實例建立後,後續調用獲取對象的方法時,直接返回對象,優勢:線程安全、延遲加載、高效。

  這裏必需要插播一下volatile這個java關鍵字:volatile關鍵字用以聲明變量的值可能隨時會別的線程修改,使用volatile修飾的變量會強制將修改的值當即寫入主存,主存中值的更新會使緩存中的值失效。volatile具有可見性和有序性,不具有原子性。

  • 可見性:多個線程訪問一個變量x,A線程修改了變量x的值,其餘B線程、C線程。。。當即能夠讀取到A線程修改x後的值;
  • 有序性:即程序執行時按照代碼書寫的前後順序執行。在Java內存模型中,容許編譯器和處理器對指令進行重排序,可是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。而volatile會禁止指令重排,從而保證有序性。
  • 原子性:與事務相關的概念,表示一系列操做要麼都執行,要麼一個也不執行。若是隻執行了一部分,那麼須要回滾已執行的代碼。olatile不具有原子性,這是volatile與java中的synchronized、java.util.concurrent.locks.Lock最大的功能差別。雖然volatile不能保證原子性,可是它也不阻塞線程,所以它的響應速度比synchronized快。volatile適用於讀多寫少的場景,或者對變量的寫操做不依賴於當前值,對變量的讀取操做不依賴於非volatile變量的場景。

三、第三種寫法:靜態內部類

package com.designpattern.cn.singletonpattern;

public class LazySingletonStaticInnerClass {
    private LazySingletonStaticInnerClass(){}
    //靜態內部類
    private static class singletonInstance{
        private static final LazySingletonStaticInnerClass INSTANCE = new LazySingletonStaticInnerClass();
    }

    public static  LazySingletonStaticInnerClass getInstance(){
        return singletonInstance.INSTANCE;
    }
}
View Code

  這種寫法比較推薦,這種方式看上去像是餓漢模式,實際上有區別,因爲靜態內部類的特性,在外部類加載時並不會立刻實例化,只有在調用getInstance方法時,纔會實例化類。這裏,JVM幫助咱們保證了線程安全性,當JVM進行類的初始化時,其餘線程是沒法進入的。保證了線程安全、延遲加載,效率高。

四、第四種寫法:利用枚舉類(實現單例模式的最佳作法)

  建立一個對象的方式有多種:new,克隆,序列化,反射。前三種狀況,上面的餓漢模式和線程安全的懶漢模式均可以保證生成實例的惟一性,可是對於最後一種——反射,沒法保證明例的惟一性:經過反射能夠獲取到類的構造方法(即便聲明構造方法是private的也沒用,反射能夠打破一切封裝,可是枚舉例外)

package com.designpattern.cn.singletonpattern;

public enum SingletonEnum {
    INSTANCE;
}
View Code

  可使用SingletonEnum.INSTANCE方式獲取枚舉的實例。因爲枚舉的構造方式和單例模式很像(構造方法私有化),並且不用考慮序列化等問題。所以使用枚舉來構建單例模式是目前最好的作法,而且不被反射打破。(能夠參考閱讀《Effective Java》)

6、相關的模式

  • 抽象工廠(AbstractFactory)模式
  • Builder模式
  • Fcade外觀模式
  • Prototype原型模式
相關文章
相關標籤/搜索