1. 程式人生 > >執行緒安全的單例模式也可以很精彩

執行緒安全的單例模式也可以很精彩

這篇部落格以多種方式實現單例模式,包括非執行緒安全、執行緒安全的單例模式以及執行緒安全的優化。
餓漢式單例;
懶漢式單例(延遲初始化);(執行緒不安全)
執行緒安全的單例-synchronized方法
執行緒安全的單例-同步程式碼塊
執行緒安全的單例-顯式鎖ReentrantLock進一步減小鎖的粒度
執行緒安全的單例-DCL(double check lock)

首先可以參考一下我的另外一篇博文了解什麼是單例模式:
單例模式

多執行緒測試獲取單例例項

為了模擬在多執行緒的高併發下同時獲取單例中是例項的情況,我寫了一個測試用例,使用了J.U.C 併發包下的欄珊,使用欄珊的目的是為了讓100個執行緒到達欄珊後互相等待,知道全部到達欄珊然後開放出口,100個執行緒同時發起獲取單例。具體的程式碼如下:

package patterns.singleton_multi;

import java.util.Random;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Created by louyuting on 17/1/22.
 * 多執行緒測試端-使用欄珊實現100各執行緒同時申請建立單例
 */
public class TestClient { private static final Integer THREAD_NUM = 100; static class NewInstance implements Runnable{ private String name; private CyclicBarrier barrier;//欄珊 public NewInstance(CyclicBarrier barrier, String name) { super(); this
.barrier = barrier; this.name = name; } @Override public void run() { try { //做個延時 TimeUnit.MILLISECONDS.sleep(1000 * (new Random()).nextInt(8)); System.out.println(name + " 準備好了..."); //barrier的await方法,在所有參與者都已經在此 barrier 上呼叫 await 方法之前,將一直等待。 barrier.await(); } catch (Exception e) { e.printStackTrace(); } System.out.println(name + "的例項的hashcode:"+ Singleton1.getInstance().hashCode()); } } /** * 測試類的主函式 * @param args */ public static void main(String[] args) { CyclicBarrier barrier = new CyclicBarrier(THREAD_NUM, new Runnable() { @Override public void run() { System.out.println("--------------提示:所有執行緒都已經到達.可以執行開始獲取單例了.--------------------"); } }); ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM); for(int i=0; i<THREAD_NUM; i++){ if(i<10){ executor.submit(new NewInstance(barrier, "thread-0"+i)); }else{ executor.submit(new NewInstance(barrier, "thread-"+i)); } } executor.shutdown(); } }

1.餓漢式單例

餓漢式單例是指在方法呼叫前,例項就已經建立好了,所以肯定是執行緒安全的。下面是實現程式碼:

package patterns.singleton_multi;

/**
 * Created by louyuting on 17/1/22.
 * 餓漢式單例模式
 */
public class Singleton1 {
    //單例
    private static Singleton1 instance = new Singleton1();

    //構造器私有,不開放
    private Singleton1(){

    }

    public static Singleton1 getInstance(){
        return instance;
    }
}

使用測試用例的測試結果如下:
這裡寫圖片描述

從執行結果可以看出例項變數額hashCode值一致,這說明物件是同一個,餓漢式單例實現了。

2.懶漢式單例(延遲初始化);

懶漢式單例是指在方法呼叫獲取例項時才建立例項,因為相對餓漢式顯得“不急迫”,所以被叫做“懶漢模式”。下面是實現程式碼:

package patterns.singleton_multi;

import java.util.concurrent.TimeUnit;

/**
 * Created by louyuting on 17/1/22.
 * 懶漢式單例(延遲初始化); 多執行緒下完全不安全
 */
public class Singleton2 {
    //單例
    private static Singleton2 instance;

    //構造器私有,不開放
    private Singleton2(){

    }

    public static Singleton2 getInstance(){
        if(instance == null){
            //建立例項前一些耗時操作
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton2();
        }
        return instance;
    }
}

測試用例測試結果如下:
這裡寫圖片描述

從執行結果可以看出例項變數的hashCode值完全一致,這說明物件不是同一個,所以是執行緒不安全的。

3.執行緒安全的單例-synchronized方法

上面的懶漢式單例明顯是執行緒不安全的,要想實現執行緒安全的實現,最簡單的方式就是使用同步原語實現synchronized方法。出現非執行緒安全問題,是由於多個執行緒可以同時進入getInstance()方法,那麼只需要對該方法進行synchronized的鎖同步即可:
原始碼如下:

package patterns.singleton_multi;

import java.util.concurrent.TimeUnit;

/**
 * Created by louyuting on 17/1/22.
 * synchronized 同步方法實現執行緒安全
 */
public class Singleton3 {
    //單例
    private static Singleton3 instance;

    //構造器私有,不開放
    private Singleton3(){

    }

    public synchronized static Singleton3 getInstance(){
        if(instance == null){
            //建立例項前一些耗時操作
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton3();
        }
        return instance;
    }
}

測試用例的結果是:
這裡寫圖片描述
很顯然實現了執行緒安全。但是這種實現方式的執行效率會很低。同步方法效率低,那我們考慮使用同步程式碼塊來實現:

4.執行緒安全的單例-同步程式碼塊

原始碼如下:

package patterns.singleton_multi;

import java.util.concurrent.TimeUnit;

/**
 * Created by louyuting on 17/1/22.
 * synchronized 同步程式碼塊實現執行緒安全
 */
public class Singleton4 {
    //單例
    private static Singleton4 instance;

    //構造器私有,不開放
    private Singleton4(){

    }

    public static Singleton4 getInstance(){
        //建立例項前一些耗時操作
        try {
            synchronized (Singleton4.class){
                if(instance==null){
                    TimeUnit.MILLISECONDS.sleep(300);
                    instance = new Singleton4();
                }else {
                    //instance非空
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

執行結果如下:
這裡寫圖片描述

同步程式碼塊雖然提交了效率,但是同步程式碼塊把方法內所有的程式碼全部包含在內,效率依然低,還可以優化,接下來我們繼續減小鎖的粒度,使用顯式鎖ReentrantLock進一步減小鎖的粒度

5.執行緒安全的單例-顯式鎖ReentrantLock

我們使用顯式鎖ReentrantLock只鎖住關鍵的程式碼行,原始碼如下:

package patterns.singleton_multi;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by louyuting on 17/1/22.
 * synchronized 同步程式碼塊實現執行緒安全
 */
public class Singleton5 {
    //單例
    private static Singleton5 instance;

    private static Lock lock = new ReentrantLock();

    //構造器私有,不開放
    private Singleton5(){

    }

    public static Singleton5 getInstance(){
        //建立例項前一些耗時操作
        try {
            lock.lock();//加鎖
            if(instance==null){
                TimeUnit.MILLISECONDS.sleep(300);
                instance = new Singleton5();
            }else {
                //instance非空
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        return instance;
    }
}

測試結果如下:
這裡寫圖片描述
在小粒度下同樣實現了執行緒安全,那麼還能不能優化呢?當然是可以的,我們知道volatile是最輕量級的鎖機制了,那能不能利用volatile來實現呢?接下來就提出了DCL方法。

6.執行緒安全的單例-DCL(double check lock)

為了達到執行緒安全,又能提高程式碼執行效率,我們這裡可以採用DCL的雙檢查鎖機制來完成,DCL的實現將鎖的粒度降低到了最小,僅僅加鎖new例項這一行程式碼,通過最輕量級的volatile實現多執行緒情況下的記憶體可見性。程式碼實現如下:

package patterns.singleton_multi;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by louyuting on 17/1/22.
 * synchronized DCl
 */
public class Singleton6 {
    //使用volatile關鍵字保其可見性
    volatile private static Singleton6 instance;

    private static Lock lock = new ReentrantLock();

    //構造器私有,不開放
    private Singleton6(){

    }

    public static Singleton6 getInstance(){
        //建立例項前一些耗時操作
        try {
            if(instance==null){
                lock.lock();//加鎖
                if(instance == null){//二次檢查  
                    TimeUnit.MILLISECONDS.sleep(300);
                    instance = new Singleton6();
                }
                lock.unlock();
            }else {
                //instance非空
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {

        }
        return instance;
    }
}

檢視測試結果:
這裡寫圖片描述

從執行結果來看,該中方法保證了多執行緒併發下的執行緒安全性。
這裡在宣告變數時使用了volatile關鍵字來保證其執行緒間的可見性;在同步程式碼塊中使用二次檢查,以保證其不被重複例項化。集合其二者,這種實現方式既保證了其高效性,也保證了其執行緒安全性。

自此,優化過程結束了,如果有路過的大佬有更好的優化策略,歡迎私信交流。我今天也是被師兄問到了這個,被吊打才有了這篇博文,歡迎各路大神一起來學習。

本文全部程式碼均可通過github獲得,github地址