單例模式屬於建立型設計模式。設計模式
確保一個類只有一個實例,並提供該實例的全局訪問點。安全
實現: 使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。多線程
類圖:併發
私有構造函數保證了不能經過構造函數來建立對象實例,只能經過公有靜態函數返回惟一的私有靜態變量。jvm
下面的實現中,私有靜態變量 uniqueInstance
被延遲實例化,這樣作的好處是,若是沒有用到該類,那麼就不會實例化 uniqueInstance
,從而節約資源。分佈式
這個實如今多線程環境下是不安全的,若是多個線程可以同時進入 if (uniqueInstance == null)
,而且此時 uniqueInstance == null
,那麼會有多個線程執行 uniqueInstance = new Singleton();
語句,這將致使實例化屢次 uniqueInstance
。函數
// 懶漢式: 線程不安全
// 有延遲加載: 不是在類加載的時候就建立了,而是在調用newStance()的時候纔會建立
public class Singleton { private static Singleton uniqueInstance; private Singleton(){ } public static Singleton newInstance(){ if(uniqueInstance == null) uniqueInstance = new Singleton(); return uniqueInstance; } }
爲了解決上面的問題,咱們能夠直接在newInstance()
方法上面直接加上一把synchronized
同步鎖。那麼在一個時間點只能有一個線程可以進入該方法,從而避免了實例化屢次 uniqueInstance
。高併發
可是當一個線程進入該方法以後,其它試圖進入該方法的線程都必須等待,即便 uniqueInstance
已經被實例化了。這會讓線程阻塞時間過長,所以該方法有性能問題,不推薦使用。性能
public static synchronized Singleton newInstance(){//在上面的基礎上加了synchronized if(uniqueInstance == null) uniqueInstance = new Singleton(); return uniqueInstance; }
餓漢式就是 : 採起直接實例化 uniqueInstance
的方式,這樣就不會產生線程不安全問題。測試
這種方式比較經常使用,但容易產生垃圾對象(丟失了延遲實例化(lazy loading
)帶來的節約資源的好處)。
它基於 classloader機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然致使類裝載的緣由有不少種,在單例模式中大多數都是調用 getInstance 方法, 可是也不能肯定有其餘的方式(或者其餘的靜態方法)致使類裝載,這時候初始化 instance 顯然沒有達到 lazyloading 的效果。
public class Singleton { // 急切的建立了uniqueInstance, 因此叫餓漢式 private static Singleton uniqueInstance = new Singleton(); private Singleton(){ } public static Singleton newInstance(){ return uniqueInstance; } // 瞎寫一個靜態方法。這裏想說的是,若是咱們只是要調用 Singleton.getStr(...), // 原本是不想要生成 Singleton 實例的,不過沒辦法,已經生成了 public static String getStr(String str) {return "hello" + str;} }
uniqueInstance
只須要被實例化一次,以後就能夠直接使用了。加鎖操做只須要對實例化那部分的代碼進行,只有當uniqueInstance
沒有被實例化時,才須要進行加鎖。
雙重校驗鎖先判斷 uniqueInstance
是否已經被實例化,若是沒有被實例化,那麼纔對實例化語句進行加鎖。
// 雙重加鎖 public class Singleton { // 和餓漢模式相比,這邊不須要先實例化出來 // 注意這裏的 volatile,使用 volatile 能夠禁止 JVM 的指令重排,保證在多線程環境下也能正常運行 private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton newInstance() { if (uniqueInstance == null) { synchronized (Singleton.class) { // 這一次判斷也是必須的,否則會有併發問題 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
注意,內層的第二次if (uniqueInstance == null) {
也是必須的,若是不加: 也就是隻使用了一個 if 語句。在uniqueInstance == null
的狀況下,若是兩個線程都執行了 if 語句,那麼兩個線程都會進入 if 語句塊內。雖然在 if 語句塊內有加鎖操做,可是兩個線程都會執行uniqueInstance = new Singleton();
這條語句,只是前後的問題,那麼就會進行兩次實例化。所以必須使用雙重校驗鎖,也就是須要使用兩個 if 語句。
volatile
關鍵字修飾也是頗有必要的, uniqueInstance = new Singleton();
這段代碼實際上是分爲三步執行:
uniqueInstance
分配內存空間;uniqueInstance
;uniqueInstance
指向分配的內存地址;可是因爲 JVM 具備指令重排的特性,執行順序有可能變成 1>3>2
。指令重排在單線程環境下不會出現問題,可是在多線程環境下會致使一個線程得到尚未初始化的實例。例如,線程 T1
執行了 1 和 3,此時 T2
調用 newInstance()
後發現 uniqueInstance
不爲空,所以返回 uniqueInstance
,但此時 uniqueInstance
還未被初始化。
使用 volatile 能夠禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。
當 Singleton
類加載時,靜態內部類 Holder 沒有被加載進內存。只有當調用 newInstance()
方法從而觸發 Holder.uniqueInstance
時 Holder
纔會被加載,此時初始化 uniqueInstance
實例,而且 JVM 能確保 uniqueInstance
只被實例化一次。
這種方式不只具備延遲初始化的好處,並且由 JVM 提供了對線程安全的支持。
這種方式是 Singleton 類被裝載了,uniqueInstance
不必定被初始化。由於Holder
s 類沒有被主動使用,只有經過顯式調用newInstance()
方法時,纔會顯式裝載 Holder 類,從而實例化uniqueInstance
。
public class Singleton { private Singleton() { } // 主要是使用了 嵌套類能夠訪問外部類的靜態屬性和靜態方法 的特性 // 不少人都會把這個嵌套類說成是靜態內部類,嚴格地說,內部類和嵌套類是不同的,它們能訪問的外部類權限也是不同的。 private static class Holder { private static final Singleton uniqueInstance = new Singleton(); } public static Singleton newInstance() { return Holder.uniqueInstance; } }
這種實現方式尚未被普遍採用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止屢次實例化。
該實如今屢次序列化再進行反序列化以後,不會獲得多個實例。而其它實現須要使用 transient
修飾全部字段,而且實現序列化和反序列化的方法。
枚舉實現單例 (+測試):
public class Singleton { private Singleton() { } public static Singleton newInstance() { return Sing.INSTANCE.newInstance(); } private enum Sing { INSTANCE; private Singleton singleton; //jvm guarantee only run once Sing() { singleton = new Singleton(); } public Singleton newInstance() { return singleton; } } public static int clientTotal = 1000; public static int threadTotal = 200; public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); Semaphore semaphore = new Semaphore(threadTotal); CountDownLatch countDownLatch = new CountDownLatch(clientTotal); Set<Singleton>set = Collections.synchronizedSet(new HashSet<>());//注意set也要加鎖 for (int i = 0; i < clientTotal; i++) { executorService.execute(() -> { try { semaphore.acquire(); set.add(Singleton.newInstance()); semaphore.release(); } catch (Exception e) { } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(set.size());//1 } }
關於序列化和反序列化:
public enum Singleton { INSTANCE; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
測試:
public class Test { public static void main(String[] args){ // 單例測試 Singleton s1 = Singleton.INSTANCE; s1.setName("firstName"); System.out.println("s1.getName(): " + s1.getName()); Singleton s2 = Singleton.INSTANCE; s2.setName("secondName"); //注意我這裏輸出s1 ,可是已經變成了 secondName System.out.println("s1.getName(): " + s1.getName()); System.out.println("s2.getName(): " + s2.getName()); System.out.println("-----------------"); // 反射獲取實例測試 Singleton[] enumConstants = Singleton.class.getEnumConstants(); for (Singleton enumConstant : enumConstants) System.out.println(enumConstant.getName()); } }
輸出:
s1.getName(): firstName s1.getName(): secondName s2.getName(): secondName ----------------- secondName
該實現能夠防止反射攻擊。在其它實現中,經過
setAccessible()
(反射中的強制訪問私有屬性方法) 方法能夠將私有構造函數的訪問級別設置爲 public,而後調用構造函數從而實例化對象,若是要防止這種攻擊,須要在構造函數中添加防止屢次實例化的代碼。該實現是由 JVM 保證只會實例化一次,所以不會出現上述的反射攻擊。
通常狀況下,不建議使用懶漢方式,建議使用餓漢方式。
只有在要明確實現 lazy loading
效果時,纔會使用靜態內部類方式。
若是涉及到反序列化建立對象時,能夠嘗試使用枚舉方式。
若是有其餘特殊的需求,能夠考慮使用雙檢鎖方式。
免費Java高級資料須要本身領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q