天天看點

設計模式之——單例設計

轉載請說明來自:http://blog.csdn.net/super_kingking/article/details/51277238

前言:我們在開發過程中都會用到單例設計模式,但是為什麼我們要用單例呢?單例設計模式的有點和缺點以及單利設計模式的幾種形式?

我們為什麼要用單例呢?因為單例可以減輕加載的負擔,縮短加載的時間,提高加載的效率。

單例适用的方面:

  1. 控制資源的使用,通過線程同步來控制資源的并發通路。
  2. 控制執行個體的産生,以達到節約資源的目的。
  3. 通過資料共享,在不建立直接關聯的條件下,讓多個不相關的線程之間通信。

首先我們先來看一下單例設計模式的寫法:

餓漢式設計模式

public class Singleton{
    //在自己内部定義自己的一個執行個體,隻供内部調用
    private static final Singleton instance = new Singleton();
    private Singleton(){

    }
    //這裡提供了一個供外部通路本class的靜态方法,可以直接通路
    public static Singleton getInstance(){
        return instance;
    }
}
           

餓漢就是類一旦加載,就把單例初始化完成,保證getInstance的時候,單例是已經存在的了。但是如果我沒有調用getInstance方法,instance執行個體已經存在,這樣造成不必要的資源消耗,肯定希望在需要的時候才去建立instance對象。

懶漢式

public class SingleTon {
    //instance需要用static修飾,在static方法中,引用的變量類型必須也是static類型的

    private static SingleTon instance;
    //構造函數要private修飾,私有化,隻能在SingleTon類中建立執行個體化,避免在外部類中進行執行個體化。
    private SingleTon() {
    }

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

Singleton的唯一執行個體隻能通過getInstance()方法通路。這樣的代碼如果在多個線程當中是不安全的,是以我們需要進行改進。

**getInstance方法上加同步**
public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {
    }

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

這樣的已經很安全,有效的解決了可能會在多個線程中建立出多個instance對象,但是現在又存在一個新的問題,在方法上添加synchronized同步,每次在調用getInstance的時候都會受到同步鎖的影響,這樣是非常影響效率的。接下來我們再進行改進。

//我們将synchronized加到方法體中
public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {
    }

    public static SingleTon getInstance() {
    //但是在這裡添加和上面的效果其實是一樣的,我們還需要進行下一句的優化
        synchronized(SingleTon.class)  {

            if (instance== null) {
                instance= new SingleTon();
            }
            return instance;
        }
      }
    }
           

DCL (Double Check Lock)雙重檢查鎖定

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

這種雙重鎖定的模式,代碼再進入第6行的時候,首先會判斷instance是否為空,避免了不必要的同步,第2個判斷則是為了在null的情況下建立執行個體,這種方式叫做雙重鎖定單例。

下面分析一下DCL的步驟,當線程1執行了 instance = new SingleTon()語句,這句話看起來是一句話,但是實際上它做了3件事:

  • (1)首先給instance執行個體配置設定記憶體空間(空的記憶體)
  • (2)調用new SingleTon()構造函數,初始化成員字段
  • (3)将instance引用指向記憶體配置設定的空間位址,此時的instance已經不在是null的了。

JVM編譯器的指令重排序,并不是随着代碼的一條條順序執行的,是以會出現1-2-3的執行順序,也會出現1-3-2的執行順序,如果是1-3-2的執行順序,3執行完畢,2未執行前,切換到線程2中去,instance此時已經指向了記憶體配置設定的位址,已經不為null,但是傳回的是個沒有初始化完成的instance對象,這時線上程2中使用就會出錯。

在JDK1.5之後修改了這個bug,sun公司提供了volatile關鍵字:1.可見性:線程在每次使用變量的時候,都會從主記憶體中讀取變量修改後的值。2.阻止指令重排序

将instance修改定義為private volatile static SingleTon instance = null,

public class Singleton {
    private Singleton() {}  //私有構造函數
    private volatile static Singleton instance = null;  //單例對象
    //靜态工廠方法
    public static Singleton getInstance() {
          if (instance == null) {      //雙重檢測機制
         synchronized (Singleton.class){  //同步鎖
           if (instance == null) {     //雙重檢測機制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}
           

當線程1執行 instance = new SingleTon()的時候,JVM的執行順序必須是

- (1)首先給instance執行個體配置設定記憶體

- (2)調用new SingleTon()構造函數,初始化成員字段

- (3)将instance引用指向記憶體配置設定的空間位址。

線上程2看來,instance對象要麼指向一個初始化完整的instance對象,要麼是null,不會出現中間值。

總結: volatile關鍵字不但可以防止指令重排,也可以保證線程通路的變量值是主記憶體中的最新值

靜态内部類單例模式:

DCL (Double Check Lock)雖然在一定程度上是解決了資源消耗,多餘的同步,線程安全等問題,但是在高并發環境或者jdk1.6版本下使用下也會有一定的缺陷,java記憶體模型的原因偶爾也是會失敗的,但是機率很小。如果使用靜态内部類單例模式則安全,高效,唯一:

public class SingleTon {
    private SingleTon (){

    }
    public static Singleton getInstance(){
        return SingleTonHolder.singleTon ;
    }
    private static class SingleTonHolder{
        private static final SingleTon singleTon  = new SingleTon();
    }
}
           

第一此加載Singleton類時并不會初始化instance,隻有在第一次調用getInstance()方法時虛拟機才會加載SingleTonHolder類,然後對instance進行初始化,該形式是利用了ClassLoader的加載機制來實作懶加載,這種方式不僅能夠保證線程的安全,而且能夠保證單例對象的唯一性,同時還延遲了單例的執行個體化,是以更推薦這種方式。

缺點:雙重檢查機制和靜态内部類雖然很好,但是依然無法防止利用反射來重複建構對象。

利用反射建構單例對象:

//獲得構造器
Constructor con = Singleton.class.getDeclaredConstructor();
//設定為可通路
con.setAccessible(true);
//構造兩個不同的對象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//驗證是否是不同對象
System.out.println(singleton1.equals(singleton2));//false
//得到的結果是false,說明這裡是2個不同的對象。
           

那怎麼才能阻止反射的建構方式呢?:枚舉實作單例設計模式

public enum SingletonEnum {
    INSTANCE;
}
           

使用枚舉單例不但能保證防止反射構造對象,而且也能保證線程安全,但是其并不是使用線程安全,單例對象INSTANCE是在枚舉類被加載的時候進行初始化的。