1. 程式人生 > >日常小結-多執行緒的單例模式的三種實現方式

日常小結-多執行緒的單例模式的三種實現方式

多執行緒單例模式在很多併發的書裡面都有寫。這裡做一個簡單的總結。主要內容來自《java併發程式設計的藝術》《java多執行緒程式設計核心》

單例模式的分類

餓漢模式:類初始化的時候就進行建立單例模式
懶漢模式:在呼叫getinstance方法的時候才建立單例;

首先餓漢模式是不會出現任何問題的。而且在大多數情況下餓漢模式要由於懶漢模式。只有在少數情況下,增快應用啟動速度或者減少初始化延遲的時候才使用懶漢模式。典型的懶漢模式的解決方案是DCL。除此之外還有一種方案是使用靜態類初始化方案。

餓漢模式

餓漢模式很簡單在靜態域內進行初始化。不會出現任何問題

package test;
public
class MyObject{ private volatile static MyObject myObject = new MyObject(); private MyObject(){}; }

類初始化的過程可詳見《深入理解java虛擬機器》。

懶漢模式

在多執行緒的情況下可能會有多個執行緒同時呼叫getinstance這樣會導致建立多個例項。

解決方案

synchronized

synchronized方法來同步方法或者同步整個getinstance。
當然是有效的只是效率比較低。
需要說明的是synchronized方法只對new方法進行同步是不可行的,因為在檢測null之後可能會有執行緒切換。

DCL雙重檢測鎖

首先對UniqueInstance引用加速volatile變數。
然後在在對new函式進行同步,並進行雙重檢測,這是目前通用的高效解決方案。

下面貼一下程式碼:

package test;
public class MyObject{
    private volatile static MyObject myObject;
    private MyObject(){};

    public static MyObject getInstance(){
        try{
            if(myObject == null){
                Synchronized(MyObject.class){
                    if
(myObject == null){ myObject = new MyObejct(); } } } }catch (InterruptedException e){ e.printStackTrace(); } return myObject; } }
package test;
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Run{
    public static void main(String[] args){
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

類初始化的解決方案

從深層次的角度說之所以出現單例模式失效的原因不僅僅是因為執行緒間的切換,還有是因為編譯器和處理器在執行java程式碼的時候會進行重排序。從而導致沒有良好同步的語句之間程式碼的執行順序不是按照程式設計師預想的那樣。
使用類初始化的原理就是在執行類初始化的過程中,當前執行緒需要獲得類初始化的鎖。這樣只有獲得了類初始化的鎖的程式才能進行初始化。及時在本執行緒內初始化的語句產生了重排序但是在獲得鎖和釋放鎖之間的程式屬於臨界區內,對外界而言是不可見的。因此也不會產生多個例項的結果。
簡而言之,如果在類的內部構造一個靜態的類,那這個類的初始化只會由一個獲得了這個類初始化鎖的程序執行。
程式碼如下:

package test;
public class InstanceFactory{
    private static class InstanceHolder{
        public static Instance instance = new Instance();
    }

    public static Instance getInstance(){
        return InstanceHolder.isntance;
    }

這樣的類初始化方案顯得比較簡潔,但是相比於volatile的方案這樣的初始化只能進行靜態類變數的訪問,對於例項變數的內容則無能無力了

遇到的坑

本來這方面的內容應該在稍後的時間看,但是最近做東西遇到了這個問題。
最近在用netty寫一個im的程式。這裡面用到資料。我把資料庫的連線專門建了一個類然後用單例模式進行管理,但是後來發現不可用。找了很久沒發現問題,後來才查到這方面的資料。
本來問題應該已經解決了,但是我在寫這個多執行緒的demo的時候出現了始終沒辦法用,主要原因還是有個基本的問題沒有解決。
通常來說多執行緒單例模式的情況下是4個類,一個單例模式本身的類,一個tes1和test2用來呼叫getInstance,一個test3用來執行test1和test2。但是我在用的時候沒有寫test3。而是test1和test2直接執行。總是會建立兩個例項,原因很簡單。所謂單例模式也好,或者任何其他記憶體一致性問題通常來說都是要在一個程序內的多個執行緒之間的共享記憶體及通訊問題。但是直接執行test1和test2則可以看成是兩個程序。而使用test3呼叫則是一個程序。