多線程環境下的單例模式


單線程環境下的單例實現運行在多線程環境下會出現問題(volatile也只能保證可見性,並不能保證原子性)。html

package com.prac;
import java.util.ArrayList;

//單線程下的單例實現
class Singleton{
    private volatile static Singleton instance;
    public static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
public class TestSingletonOfMT{
    private static ArrayList<Singleton> list = new ArrayList<Singleton>();    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Singleton singleton = Singleton.getInstance();
                    synchronized (list) {//保證add操做的原子性
                        list.add(singleton);
                    }
                }
            }).start();
        }
        //等待全部的子線程執行結束,尚有子線程在執行,就將主線程處於就緒等待
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println("list.size() = "+list.size());
        for (int i = 0; i < list.size(); i++) {
            for (int j = i+1; j < list.size(); j++) {
                if (!(list.get(i) == list.get(j))) {
                    System.out.println("list["+i+"]" +" != "+"list["+j+"]");
                    System.out.println("list["+i+"] = "+list.get(i));
                    System.out.println("list["+j+"] = "+list.get(j));
                }
            }
        }
        
    }

}

以上示例代碼在個人運行環境下輸出以下:java

list.size() = 5
list[0] != list[2]
list[0] = com.prac.Singleton@525483cd
list[2] = com.prac.Singleton@2a9931f5
list[0] != list[3]
list[0] = com.prac.Singleton@525483cd
list[3] = com.prac.Singleton@2a9931f5
list[0] != list[4]
list[0] = com.prac.Singleton@525483cd
list[4] = com.prac.Singleton@2a9931f5
list[1] != list[2]
list[1] = com.prac.Singleton@525483cd
list[2] = com.prac.Singleton@2a9931f5
list[1] != list[3]
list[1] = com.prac.Singleton@525483cd
list[3] = com.prac.Singleton@2a9931f5
list[1] != list[4]
list[1] = com.prac.Singleton@525483cd
list[4] = com.prac.Singleton@2a9931f5

代表多個線程去獲取單實例獲得的卻不是同一個對象,違背了單實例模式的初衷。其緣由在於,以下代碼不能保證原子性:安全

        if (instance == null) {
            instance = new Singleton();
        }

同步機制是一種可行的改進策略,採用synchronized代碼塊改進成以下實現方式。多線程

//多線程下的單例實現
class Singleton{
    private volatile static Singleton instance;
    public static Singleton getInstance(){
        if (instance == null) {
            //這個地方有可能存在線程切換
            synchronized (Singleton.class) {
                //必需要再次進行null值檢測,由於在synchronized代碼塊和第一處null值檢測之間,可能會有線程切換。
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

這種實現稱爲雙檢鎖(DCL:Double-Checked Locking)實現。與靜態域直接初始化方式實現相比,可以起到懶加載的效果。app

實際上雙檢鎖實現出現過不少陷阱,最明顯的就是指令重排序致使的雙檢鎖失效問題:ide

instance = new Singleton();並不是是原子操做,在指令級別層面對應了多條彙編指令,所以編譯器和處理器有機會對其進行指令重排序。這就有可能致使在將引用變量instance指向堆內存中對象的存儲區域時,Singleton對象並未真正初始化完成,也就是說此時instance引用到的對象是不可用、不完整的狀態。this

volatile修飾instance有如下兩層語義:spa

一、保證instance變量的可見性(在彙編指令層面增長lock指令前綴)。線程

二、禁止指令重排序。code

在JDK1.5中修復了volatile屏蔽指令重排序的語義後,搭配volatile關鍵字纔可以安全的用DCL實現單例模式。JDK1.5以前仍然不能用volatile修復DCL失效的問題。

引文:



 

// double-checked-locking - don't do this!
private static Something instance = null; public Something getInstance() { if (instance == null) { synchronized (this) { if (instance == null) instance = new Something(); } } return instance; }

The most obvious reason is that the writes which initialize instance and the write to the instance field can be reordered by the compiler or the cache, which would have the effect of returning what appears to be a partially constructed Something. The result would be that we read an uninitialized object. There are lots of other reasons why this is wrong, and why algorithmic corrections to it are wrong. There is no way to fix it using the old Java memory model.



 

另一種類似的改進方式以下(或者直接用synchronized修飾getInstance()方法)。雖然也可以正確的工做,可是這樣的實現效率偏低,由於每個線程調用getInstance()方法是都會先加鎖,以後才進行null值檢測,其實除了第一次調用getInstance()方法獲取單例對象時有加鎖的必要性外,以後無需加鎖,只須要進行null值檢測便可,這也是DCL實現中用兩次null檢測的緣由。

//多線程下的單例實現
class Singleton{
    private volatile static Singleton instance;
    public static Singleton getInstance(){
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

另一個線程安全的延遲初始化解決方案是利用類加載階段的初始化來實現(被稱爲Initialization On Demand Holder idiom)

class Singleton{
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;//LazyHolder類被初始化
    }
}

 

若是摒棄懶加載的優點,能夠更簡單的用靜態域直接初始化的方式實現:

class Singleton {
    static Singleton singleton = new Singleton();
}

 

參考資料:

一、https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

二、https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl

相關文章
相關標籤/搜索