單例模式——8種實現方式

前言

在一些場景中,咱們但願建立的對象在整個軟件系統只保存一份實例,如線程池, 日誌對象、緩存等。建立並保存對象單一主要有兩個做用:節省系統資源;防止多個對象產生衝突。**單例模式(Singleton Pattern)**就能夠確保只有一個實例對象會被建立。今天,咱們重點聊聊單例模式的8種實現方式(java語言)java

private修飾符

咱們都知道,能夠經過 new 的方式建立對象。若是類對new方式建立對象不加以約束的話,就不能保證系統只建立一個對象。private修飾類的構造方法,就能夠確保該類不能任意建立對象。緩存

1、餓漢式(靜態常量)

餓漢式實現單例模式的原理:利用靜態常量在類加載時生成全局惟一實例特性安全

具體代碼markdown

// 單例模式實現1,餓漢式(靜態常量)
public class Singleton1 {
    // 類加載時,實例化對象
    private static Singleton1 instance = new Singleton1();

    public static Singleton1 getInstance() {
        return instance;
    }

    private Singleton1() {
        System.out.println("單例模式實現1,餓漢式(靜態常量)");
    }

    public static void main(String[] args) {
        System.out.println("開始演示靜態常量方式建立單例對象:");
        Singleton1 instance1 = Singleton1.getInstance();
        Singleton1 instance2 = Singleton1.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 運行main方法,結果以下:
單例模式實現1,餓漢式(靜態變量)
開始演示靜態變量方式建立單例對象:      
true
複製代碼

從運行結果(輸出打印的1,2行順序)能夠看出,咱們想要獲取的實例對象在真正獲取以前已經實例化。(靜態常量在類加載過程當中賦值)多線程

這也是餓漢式實現單例模式很差的一點:不能懶加載。jvm

2、餓漢式(靜態代碼塊)

基本與上面的實現方式同樣,只是語法有點區別,靜態代碼塊替換靜態變量直接賦值。oop

// 單例模式實現2,餓漢式(靜態代碼塊)
public class Singleton2 {

    static {
        instance = new Singleton2();
    }

    private static Singleton2 instance;

    private Singleton2() {
        System.out.println("單例模式實現2,餓漢式(靜態代碼塊)");
    }

    public static Singleton2 getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("開始演示靜態代碼塊方式建立單例對象:");
        Singleton2 instance1 = Singleton2.getInstance();
        Singleton2 instance2 = Singleton2.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 運行main方法,結果以下:
單例模式實現2,餓漢式(靜態代碼塊)
開始演示靜態代碼塊方式建立單例對象:
true
複製代碼
3、懶漢式(常規寫法,線程不安全)

上面的兩種寫法,都是不支持懶加載的。接下來的幾種方式,都是懶加載的方式。首先看看最簡單的一種實現方式優化

// 單例模式實現3,懶漢式(常規寫法,線程不安全)
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {
        System.out.println("單例模式實現3,懶漢式(常規寫法,線程不安全)。當前線程:" + Thread.currentThread().getName());
    }

    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("開始演示常規懶加載方式建立單例對象:");
        Singleton3 instance1 = Singleton3.getInstance();
        Singleton3 instance2 = Singleton3.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 運行結果
開始演示常規懶加載方式建立單例對象:
單例模式實現3,懶漢式(常規寫法,線程不安全)。當前線程:main
true
複製代碼

從運行結果來看,這種方式彷佛沒有問題。即實現了懶加載,又保證了對象單一。spa

咱們換種演示方式,修改main方法:線程

public static void main(String[] args) {
        System.out.println("開始演示常規懶加載方式建立單例對象:");
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton3.getInstance();
            }).start();
        }
    }
    // 運行結果(有可能須要多運行幾回,纔會出現相似效果)
    開始演示常規懶加載方式建立單例對象:
	單例模式實現3,懶漢式(常規寫法,線程不安全)。當前線程:Thread-1
	單例模式實現3,懶漢式(常規寫法,線程不安全)。當前線程:Thread-0
複製代碼

從運行結果能夠看出,這種單例模式的實現方式是線程不安全的,在多線程環境下,有可能會建立多個實例。

4、懶漢式(同步方法,線程安全)

方式三建立單例對象,線程不安全的緣由是:當instance在完成實例化以前,多個線程同時判斷if (instance == null)結果都爲true,致使這些線程都往下繼續執行建立實例對象。簡單粗暴的解決方式,在getInstance方法加鎖(用synchronized關鍵字修飾方法)。具體代碼:

// 單例模式實現4,懶漢式(同步方法,線程安全)
public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {
        System.out.println("單例模式實現4,懶漢式(同步方法,線程安全)。當前線程:" + Thread.currentThread().getName());
    }

    public static synchronized Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("開始演示懶加載-同步方法方式建立單例對象:");
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton4.getInstance();
            }).start();
        }
    }
}
// 運行結果
開始演示懶加載-同步方法方式建立單例對象:
單例模式實現4,懶漢式(同步方法,線程安全)。當前線程:Thread-1
複製代碼

這種方式,雖然解決了線程安全問題,可是每次獲取實例對象時,都須要加鎖,這大大影響了系統運行效率。接下來的實現方式,將逐步優化線程安全下懶加載效率低的問題。

5、懶漢式(同步代碼塊)

在靜態方法加鎖,鎖粒度太大,形成資源浪費。所以,咱們嘗試把鎖粒度縮小,在代碼塊加鎖。

示例代碼:

public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {
        System.out.println("單例模式實現5,懶漢式(同步代碼塊,線程安全)。當前線程:" + Thread.currentThread().getName());
    }

    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class){
                instance = new Singleton5();
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("開始演示懶加載-同步代碼塊方式建立單例對象:");

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton5.getInstance();
            }).start();
        }
    }
}
// 運行結果(有可能須要多運行幾回,纔會出現相似效果):
開始演示懶加載-同步代碼塊方式建立單例對象:
單例模式實現5,懶漢式(同步代碼塊,線程安全)。當前線程:Thread-0
單例模式實現5,懶漢式(同步代碼塊,線程安全)。當前線程:Thread-1
複製代碼

從運行結果來看,這種實現方式也是線程不安全的。緣由分析:

關鍵代碼

if (instance == null) {                     // 第1行
            synchronized (Singleton5.class){        // 第2行
                instance = new Singleton5();        // 第3行
            }
        }
複製代碼

雖然在2行加上了鎖,但這隻保證了同一時刻,只有一個線程能夠執行第3行代碼。在第3行代碼執行前,不一樣的線程仍是能夠判斷if是true,而後執行到第2行,等待有鎖的線程釋放鎖,得到鎖以後繼續建立對象。

若要線程安全,改造以下:

public static Singleton5 getInstance() {
        synchronized (Singleton5.class) {
            if (instance == null) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
複製代碼

然而,這種實現方式與第四種效果一致,鎖的粒度是整個getInstance方法。

6、懶漢式(雙重檢查)

前面三種懶加載實現單例的方式,都有各自的不足,不是線程不安全就是獲取單例效率低。線程不安全的地方在於已有線程建立實例,繼續建立實例。效率低的地方在於,已經建立好實例,還加鎖獲取實例。而雙重檢查就避免了這兩種問題。

示例代碼

// 懶漢式(雙重檢查)
public class Singleton6 {

    private static volatile Singleton6 instance;

    private Singleton6() {
        System.out.println("單例模式實現6,懶漢式(雙重檢查)。當前線程:" + Thread.currentThread().getName());
    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            System.out.println("嘗試建立實例...");
            synchronized (Singleton6.class){
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("開始演示懶加載-雙重檢查方式建立單例對象:");

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton6.getInstance();
            }).start();
        }
    }
}
// 運行結果
開始演示懶加載-雙重檢查方式建立單例對象:
嘗試建立實例...
單例模式實現6,懶漢式(雙重檢查)。當前線程:Thread-0
複製代碼

特別注意一點,咱們靜態變量用了「volatile」關鍵詞修飾,爲何要用volatile修飾呢,能夠參考文章:《雙重檢查鎖定與延遲初始化》

7、靜態內部類

利用靜態內部類的方式,咱們也能夠實現線程安全的單例模式

示例代碼

// 單例模式實現7,靜態內部類
public class Singleton7 {

    private Singleton7(){
        System.out.println("單例模式實現7靜態內部類。當前線程:" + Thread.currentThread().getName());
    }

    private static class InstanceHolder{
        private static Singleton7 instance = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return InstanceHolder.instance;
    }

    public static void main(String[] args) {
        System.out.println("開始演示靜態內部類建立單例對象:");
        Singleton7 instance1 = Singleton7.getInstance();
        Singleton7 instance2 = Singleton7.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 運行結果
開始演示靜態內部類建立單例對象:
單例模式實現7靜態內部類。當前線程:main
true
複製代碼

JVM 幫助咱們保證了內部類建立的線程安全性

8、枚舉方式

枚舉在jvm裏是自然的單例,因此利用枚舉實現單例也是線程安全的。《 Effective Java》這本書就提倡用枚舉的方式建立單例對象

示例代碼

public enum Singleton8 {

    INSTANCE();

    Singleton8(){
        System.out.println("單例模式實現8,枚舉方式");
    }
}
複製代碼
總結

單例模式的實現方式有多種,保證線程安全和運行效率狀況下(文中的第三種方式線程不安全,第4、五種方式效率低)),各類實現方式的實際效果差異並不大,選擇本身順手的實現方式就能夠!而「懶加載」和「雙重檢查」思想,在咱們開發中常用到的,但願你們好好理解這兩種思想。

相關文章
相關標籤/搜索