單例模式屬於建立型設計模式。設計模式
確保一個類只有一個實例,並提供該實例的全局訪問點。安全
實現: 使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。bash
類圖:多線程
私有構造函數保證了不能經過構造函數來建立對象實例,只能經過公有靜態函數返回惟一的私有靜態變量。併發
下面的實現中,私有靜態變量 uniqueInstance
被延遲實例化,這樣作的好處是,若是沒有用到該類,那麼就不會實例化 uniqueInstance
,從而節約資源。jvm
這個實如今多線程環境下是不安全的,若是多個線程可以同時進入 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
的方式,這樣就不會產生線程不安全問題。ui
這種方式比較經常使用,但容易產生垃圾對象(丟失了延遲實例化(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
效果時,纔會使用靜態內部類方式。
若是涉及到反序列化建立對象時,能夠嘗試使用枚舉方式。
若是有其餘特殊的需求,能夠考慮使用雙檢鎖方式。