這是我參與8月更文挑戰的第12天,活動詳情查看: 8月更文挑戰java
單例模式(Singleton pattern):確保一個類只有一個實例,並提供該實例的全局訪問點安全
本文主要分析單例模式常見的幾種實現方式markdown
單例模式使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。多線程
私有構造函數保證了不能經過構造函數來建立對象實例,只能經過公有靜態函數返回惟一的私有靜態變量。函數
餓漢式在類加載的時候就進行實例化,這樣作的好處是線程安全;但缺點也是有的,首先在加載的時候就進行實例化,萬一這個類佔用的資源很大,就會很是浪費資源,畢竟它不必定在何時被使用,但內存是一開始就被佔用了。post
public class HungryManSingleton {
private static HungryManSingleton hungryManSingleton = new HungryManSingleton();
private HungryManSingleton() { }
public static HungryManSingleton getInstance() {
return hungryManSingleton;
}
}
複製代碼
在main方法中驗證餓漢式實現的單例模式:spa
HungryManSingleton instance1 = HungryManSingleton.getInstance();
HungryManSingleton instance2 = HungryManSingleton.getInstance();
System.out.println("從餓漢單例獲取的兩個實例比較:" + instance1.equals(instance2));
複製代碼
輸出:線程
使用反射破壞餓漢式單例模式:code
//使用反射獲取構造方法,再將構造方法的私有性破壞,而後用這個構造方法建立一個實例
Class<HungryManSingleton> singletonClass = HungryManSingleton.class;
Constructor<HungryManSingleton> declaredConstructor = singletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
HungryManSingleton instance1 = HungryManSingleton.getInstance();
HungryManSingleton instance2 = declaredConstructor.newInstance();
System.out.println("與反射獲取的實例比較:" + instance2.equals(instance1));
複製代碼
輸出:orm
能夠看到,他們並非同一個對象,這意味着餓漢式單例模式被破壞了
事實上,使用反射後,不管是餓漢式、懶漢式、升級的雙重校驗鎖機制、靜態內部類機制,都是不安全的
在懶漢式的實現中,默認不會進行實例化,何時用到了,何時 New,從而節約資源
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton() {
System.out.println(Thread.currentThread().getName());
}
public static LazySingleton getInstance() {
if (lazySingleton == null) lazySingleton = new LazySingleton();
return lazySingleton;
}
}
複製代碼
可是這個實如今多線程的環境下是不安全的,試想如下,當 lazySingleton
爲空時,試想一下,當lazySingleton
爲空時,有多個線程同時經過了if (lazySingleton == null)
的判斷,這樣就會致使new 被執行了屢次,使用代碼復現一下:
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> LazySingleton.getInstance()).start();
}
}
複製代碼
控制檯輸出:
能夠看到,實例化代碼被執行了三次,爲了解決線程安全的問題有兩個方法:
getInstance()
方法的層級上加關鍵字 synchronized
爲了解決懶漢式線程不安全的問題,能夠引入雙重校驗鎖的機制,雙重檢驗鎖也是一種延遲加載,而且較好的解決了在確保線程安全的時候效率低下的問題
如下是代碼實現:
public class DCLSingleton {
private volatile static DCLSingleton dclSingleton;
private DCLSingleton() { }
public static DCLSingleton getInstance() {
if (dclSingleton == null) {
synchronized (DCLSingleton.class) {
if (dclSingleton == null) dclSingleton = new DCLSingleton();
}
}
return dclSingleton;
}
}
複製代碼
在這個實現中,對比一下懶漢式在方法上加鎖,那麼每次調用那個方法都要得到鎖,釋放鎖,等待等待……而雙重校驗鎖鎖住了部分的代碼。進入方法若是檢查爲空才進入同步代碼塊,這樣很明顯效率高了不少
那在這裏爲何 dclSingleton == null
要判斷兩次,假設咱們先去掉第二次的判斷。
若是兩個線程一塊兒調用 getInstance()
方法,而且都經過了第一次的判斷 dclSingleton == null
,那麼第一個線程獲取了鎖,而後進行了實例化後釋放了鎖,而後第二個線程會開始執行,而後立刻也進行了實例化,這就尷尬了。
因此加上第二次判斷後,先進來的線程判斷了一下,哦?爲空,我建立一個,而後建立一個實例以後釋放了鎖,第二個線程進來以後,哎?已經有了,那我就不用建立了,而後釋放了鎖,開開心心的完成了單例模式。
對於 new 操做來講,它不是一個原子性操做,他在底層大概發生瞭如下三件事:
咱們指望它是按順序發生的,可是因爲Java的指令重排機制,可能在沒有初始化對象時,就把棧中定義的引用指給堆中的空間,當第二個線程再進來的時候,第一次斷定是否爲空,他認爲不爲空,因而將尚未進行初始化的對象返回了;這就是爲何要加上關鍵字volatile的緣由。
當 InnerClassSingleton
類加載時,靜態內部類 InnerClass
沒有被加載進內存。只有當調用 getInstance()
方法從而觸發 InnerClass.INSTANCE
時 InnerClass
纔會被加載,初始化實例 INSTANCE。
這種方式不只具備延遲初始化的好處,並且由虛擬機提供了對線程安全的支持。
public class InnerClassSingleton {
private InnerClassSingleton() { }
public static InnerClassSingleton getInstance() {
return InnerClass.INSTANCE;
}
static class InnerClass {
private static final InnerClassSingleton
INSTANCE = new InnerClassSingleton();
}
}
複製代碼
這是單例模式的最佳實踐,它實現簡單,而且在面對複雜的序列化或者反射攻擊的時候,可以防止實例化屢次
外部調用直接使用 Singleton.INSTANCE
,簡單粗暴。
因爲 Enum 實現了 Serializable 接口,因此不用考慮序列化的問題(其實序列化反序列化也能致使單例失敗的,可是咱們這裏不過多研究),而且加載的時候 JVM 能確保只加載一個實例,因此它是線程安全的,並且反射沒法破解這種單例模式的實現
public enum Singleton {
INSTANCE;
}
複製代碼
本文論述了單例模式常見的五種實現方式,在《Effect Java》中,做者極力推崇使用枚舉類來實現單例模式,並認爲這個實現是單例模式的最佳實踐
感謝閱讀,但願本文對你有所幫助