轉載請說明來自:http://blog.csdn.net/super_kingking/article/details/51277238
前言:我們在開發過程中都會用到單例設計模式,但是為什麼我們要用單例呢?單例設計模式的有點和缺點以及單利設計模式的幾種形式?
我們為什麼要用單例呢?因為單例可以減輕加載的負擔,縮短加載的時間,提高加載的效率。
單例适用的方面:
- 控制資源的使用,通過線程同步來控制資源的并發通路。
- 控制執行個體的産生,以達到節約資源的目的。
- 通過資料共享,在不建立直接關聯的條件下,讓多個不相關的線程之間通信。
首先我們先來看一下單例設計模式的寫法:
餓漢式設計模式
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是在枚舉類被加載的時候進行初始化的。