一天一個設計模式(二) - 單例模式(Singleton)

零壹技術棧發表於2018-07-13

前言

單例模式 (Singleton) 是一種建立型模式,指某個類採用Singleton模式,則在這個類被建立後,只可能產生一個例項供外部訪問,並且提供一個全域性的訪問點。

正文

(一). 優缺點

Java單例模式 (Singleton) 是一種廣泛使用的設計模式。單例模式的主要作用是保證在Java程式中,某個類只有一個例項存在。一些管理器和控制器常被設計成單例模式。

1. 優點

  • 提供了對唯一例項的受控訪問。
  • 由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件單例模式無疑可以提高系統的效能。
  • 可以根據實際情況需要,在單例模式的基礎上擴充套件做出雙例模式,多例模式。

2. 缺點

  • 單例類的職責過重,裡面的程式碼可能會過於複雜,在一定程度上違背了“單一職責原則”。
  • 如果例項化的物件長時間不被利用,會被系統認為是垃圾而被回收,這將導致物件狀態的丟失。

(二). 具體實現

簡單點說,就是一個應用程式中,某個類的例項物件只有一個,你沒有辦法去new,因為構造器是被private修飾的,一般通過getInstance()的方法來獲取它們的例項。getInstance()的返回值是一個同一個物件的引用,並不是一個新的例項。單例模式 實現起來也很容易,以下給出六種實現方式:

1. 餓漢式

特點:執行緒安全,無法實現例項懶載入策略。

public class Singleton1 {
    private final static Singleton1 singleton1 = new  Singleton1();
    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return singleton1;
    }
}
複製程式碼

2. 懶漢式

特點:執行緒不安全,實現了例項懶載入策略。

public class Singleton2 {
    private final static Singleton2 singleton2;
    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        if (singleton2 == null)
            singleton2 = new Singleton2();
        return singleton2;
    }
}
複製程式碼

3. 全域性鎖式

特點:執行緒安全,且實現了懶載入策略,但是執行緒同步時效率不高。

public class Singleton3 {
    private final static Singleton3 singleton3;
    private Singleton3() {
    }

    public synchronized static Singleton3 getInstance() {
        if (singleton3 == null)
            singleton3 = new Singleton3();
        return singleton3;
    }
}
複製程式碼

4. 靜態程式碼塊式

特點:執行緒安全,類主動載入時才初始化例項,實現了懶載入策略,且執行緒安全。

public class Singleton4 {
    private final static Singleton4 singleton4;
    private Singleton4() {
    }
    static {
        singleton4 = new Singleton4();
    }

    public static Singleton4 getInstance() {
        return singleton4;
    }
}
複製程式碼

5. 雙重校驗鎖式

特點:執行緒安全,且實現了懶載入策略,同時保證了執行緒同步時的效率。但是volatile強制當前執行緒每次讀操作進行時,保證所有其他的執行緒的寫操作已完成。volatile使得JVM內部的編譯器捨棄了編譯時優化,對於效能有一定的影響。

public class Singleton5 {
    private static volatile Singleton5 singleton5;
    private Singleton5() {
    }

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

6. 靜態內部類式【推薦】

特點:執行緒安全,不存線上程同步問題,且單例物件在程式第一次 getInstance()主動載入 SingletonHolder 和其 靜態成員 INSTANCE,因而實現了懶載入策略。

public class Singleton6 {
    private Singleton6() {
    }

    private static class SingletonHolder {
        private static final Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return Singleton6.SingletonHolder.INSTANCE;
    }
}
複製程式碼

7. 列舉方式【作者推薦】

特點:執行緒安全,不存線上程同步問題,且單例物件在列舉型別 INSTANCE 第一次引用時通過列舉建構函式 初始化,因而實現了懶載入策略。

public class Singleton7 {
    private Singleton7() {
    }

    enum SingletonEnum {
        INSTANCE;

        private final Singleton7 singleton7;

        private SingletonEnum() {
            singleton7 = new Singleton7();
        }
    }

    public static Singleton7 getInstance() {
        return SingletonEnum.INSTANCE.singleton7;
    }

    public static void main(String[] args) {
        IntStream.rangeClosed(0, 100).forEach(i -> new Thread() {
            public void run() {
                out.println(Singleton7.getInstance());
            };
        }.start());
    }
}
複製程式碼

這種方式是Effective Java作者 Josh Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還能防止反序列化重新建立新的物件,可謂是很堅強的壁壘啊。不過,由於JDK 1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏。

測試程式碼如下:

@FixMethodOrder
public class SingletonTester {
    protected final static int FROM = 0;
    protected final static int TO = 1000;

    protected static HashSet<Object> GLOBAL_SET = new HashSet<>();

    static {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                out.println();
                // count
                GLOBAL_SET.forEach((value) -> {
                    out.println("Global [" + value + "]");
                });
            }
        });
    }

    // testSingleton1
    @Test
    public void testSingleton1() throws Exception {
        final HashSet<Object> localSet = new HashSet<>();
        final CountDownLatch latch = new CountDownLatch(TO);
        IntStream.range(FROM, TO).forEach(i -> new Thread() {
            public void run() {
                Singleton1 singleton = Singleton1.getInstance();
                count(singleton);
            }

            protected void count(Singleton1 singleton) {
                localSet.add(singleton);
                out.println("Size of HashSet1 is: [" + localSet.size() + "]");
                // 計數減1,釋放執行緒
                latch.countDown();
            };
        }.start());

        // 等待子執行緒執行結束
        latch.await();

        synchronized (localSet) {
            // count
            localSet.forEach((value) -> {
                out.println("[" + value + "]");
                out.println();
            });
            GLOBAL_SET.addAll(localSet);
        }
    }

    // testSingleton2
    // testSingleton3
    // testSingleton4
    // testSingleton5
    // testSingleton6
    // testSingleton7
}

複製程式碼

測試結果截圖如下,測試用例反映7種單例模式的方案都可以正常執行:

一天一個設計模式(二) - 單例模式(Singleton)

這裡只演示其中一種單例方式,執行截圖如下:

一天一個設計模式(二) - 單例模式(Singleton)
上圖顯示,通過 getInstance() 得到的例項全域性唯一。對於其餘六中方式,根據測試用例測試得到的結果一致,大家可以自行測試。

總結

本文總結了七種Java中實現單例模式的方法,其中使用雙重校驗鎖靜態內部類列舉類 的方式可以解決大部分問題。其中,極為推薦 靜態內部類列舉類 這兩種實現方式。


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章