多線程併發下的單例模式

定義:

單例模式是設計模式中最簡單的形式之一。這一模式的目的是使得類的一個對象成爲系統中的唯一實例。

下面通過代碼分析下java中,各種單例模式寫法的優缺點。

1、餓漢模式

示例1.1

public class Singleton {
    private Singleton() {}
    private static Object INSTANCE = new Object();
    public static Object getInstance() {
        return INSTANCE;
    }
}

在類生命週期的【初始化】階段進行生成單例對象(類的初始化階段會對靜態變量賦值),當執行類初始化的階段是需要先獲得鎖才能進行初始化操作,而且一個class類只進行初始化一次。類初始化階段是線程安全的,JVM保證類初始化只執行一次。這樣可以確保只生成一個對象。

類聲明週期分爲:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸御(Unloading)。
類的生命週期不明白的請查看:JVM 類加載機制深入淺出

類加載後不一定馬上執行初始化階段。當遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。

  1. new 創建對象操作
  2. getstatic 訪問類的靜態變量操作
  3. putstatic 給類的靜態變量賦值操作
  4. invokestatic 調用靜態方法操作

這個餓漢模式中,不會出現new、invokestatic和putstatic指令,外面的類只能調用 getInstance()靜態方法,由此推斷,此單例模式也是延遲加載對象的,只有第一次調用getInstance()靜態方法,纔會觸發他的初始化階段,纔會創建單例對象。

其實這個例子應該是懶漢模式,只有在第一次使用的時候才加載

下面這個【示例1.2】不是延遲加載單例對象

示例1.2

public class Singleton {
    private Singleton() {}
    private static  int count=0;
    private static Object INSTANCE = new Object();
    public static Object getInstance() {
        return INSTANCE;
    }
}

當程序先調用Singleton1中的count屬性時(getstatic 或putstatic 指令),就會執行類的【初始化】階段,會生成單例對象,而不是調用getInstance()靜態方法才生成單例對象。

示例1.3 (靜態內部類實現方式)

public class Singleton {
    private Singleton() {}
    private static  int count=0;
    private static class SingletonHolder{
        private static final Object INSTANCE = new Object();
    }
    public static Object getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

使用內部類SingletonHolder來防止【示例1.2】出現的問題,防止其它的變量的干擾,導致提前觸發類聲明週期中的【初始化】階段來創建INSTANCE 實例。
Effective Java中推薦的單例寫法

2、懶漢模式

示例2.1

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

每次創建INSTANCE 的時候先判斷是否null,如果爲null則new一個,否則就直接返回INSTANCE 。當多線程工作的時候,如果有多個線程同時運行到if (INSTANCE == null),都判斷爲null,那麼兩個線程就各自會創建一個實例。這樣就會創建多一個實例,這樣就不是單例了。

下面的【示例2.2】加上synchronized 改進多線程併發引起的問題

示例2.2 (synchronized 實現方式)

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

雖然synchronized 能解決多線程同時併發引起的問題,但是每次訪問該方法都需要獲得鎖,性能大大降低。其實只要創建INSTANCE 實例後就不需要加鎖的,直接獲取該對象就ok。

示例2.3 (雙重檢查實現方式)

public class Singleton {
    private Singleton() {   }
    private static Object INSTANCE = null;
    public static Object getInstance() {
        if(INSTANCE == null){
            synchronized(Singleton3.class){
                if(INSTANCE == null){
                    INSTANCE = new Object();
                }
            }
        }
        return INSTANCE;
    }
}

這個版本的代碼看起來有點複雜,注意其中有兩次if (instance == null)的判斷,這個叫做『雙重檢查 Double-Check』。

第一個if (instance == null),其實是爲了解決【示例2.2】中的效率問題,只有instance爲null的時候,才進入synchronized的代碼段——這樣在對象創建後就不會在進入同步代碼塊了。
第二個if (instance == null),則是跟【示例2.2】一樣,是爲了防止可能出現多個實例的情況。

從代碼層面看似完美,效率問題也解決了。但實際還是有問題,在併發環境下可能會出現instance爲null的情況。下面我們來分析下爲什麼會出現此問題。

原子操作

INSTANCE = new Object();不是原子操作。
在JVM中會拆分成3個步驟
1、分配對象的內存空間
2、初始化對象
3、設置INSTANCE 指向剛分配的內存地址

指令重排

指令重排序是JVM爲了優化指令,提高程序運行效率,在不影響單線程程序執行結果的前提下,儘可能地提高並行度。
可以參考:java內存模型

【2、初始化對象和 3、設置INSTANCE 指向剛分配的內存地址】這兩個操作可能發生重排序。
如下圖:
指令重排序

從圖中可以看出A2和A3的重排序,將導致線程
B在B1處判斷出instance不爲空,線程B接下來將訪問instance引用的對象。此時,線程B將會訪
問到一個還未初始化的對象。

示例2.4 (基於volatile的解決方案)

public class Singleton {
    private Singleton() {}
    private static volatile Object INSTANCE = null;
    public static Object getInstance() {
        if(INSTANCE == null){
            synchronized(Singleton.class){
                if(INSTANCE == null){
                    INSTANCE = new Object();
                }
            }
        }
        return INSTANCE;
    }
}

聲明對象的引用爲volatile後,【2、初始化對象和 3、設置INSTANCE 指向剛分配的內存地址】之間的重排序,在多線程環境中將會被禁止。

volatile的重排序規則
從圖表中可以看出volatile可以確保,volatile變量讀寫順序,可以保證一個線程寫volatile變量完成後(創建完對象後),其它線程才能讀取該volatile變量,相當於給這個創建實例的構造上了一把鎖。這樣,在它的賦值完成之前,就不用會調用讀操作。

示例2.5 (枚舉實現方式)

public enum Singleton6 {
    INSTANCE;
    public String getInfo(String s){
        s = "hello " + s;
        System.out.println(s);
        return s;
    }
    public static void main(String[] args) {
        String s = INSTANCE.getInfo("aa");
        System.out.println(s);
    }
}

這種寫法在功能上與共有域方法相近,但是它更簡潔,無償地提供了序列化機制,絕對防止對此實例化,即使是在面對複雜的序列化或者反射攻擊的時候。雖然這中方法還沒有廣泛採用,但是單元素的枚舉類型已經成爲實現Singleton的最佳方法。

本人簡書blog地址:http://www.jianshu.com/u/1f0067e24ff8    
點擊這裏快速進入簡書

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