目錄
1.什么是單例模式?
2.如何保證單例?
3.兩種寫法
(1)餓漢模式(早創建)
?(2)懶漢模式(緩執行,可能不執行)
4.應用場景
🔥5.多線程中的單例模式
(1)加鎖
(2)雙重if
(3)volatie
6.指令重排序(小概率出現問題)
(1)什么是指令重排序?
?(2)以Instance = new SingletonLazy( );為例分析
(3)解決上述指令重排序問題
7.延伸:了解
🔥8.常見考察
1.什么是單例模式?
單例模式是最常見的設計模式之?
Q:啥是設計模式?
A:編程中典型場景的解決方案,設計模式好?象棋中的"棋譜".紅?當頭炮,???來跳.針對紅?的?些?法,??應招的時候有?些固定的套路.按照套路來?局勢就不會吃虧
單例模式即某個類在進程中又能有唯一實例
- 有且只有一個對象,不會new出來多個對象,這樣的對象就是“單例”
2.如何保證單例?
(1)保證單例:instance只有唯一一個,初始化也只是執行一次的
- 保證單例:instance只有唯一一個,初始化也只是執行一次的
static修飾的,其實是“類屬性”,就是在“類對象”上的,每個類的類對象在JVM中只有一個,里面的靜態成員,只有一份 - 后續需要使用這個類的實例,就可以直接通過getInstance來獲取已經new好的這個,而不是重新new
(2)?核心操作:private:禁止外部代碼來創建該類的實例
- 怎么操作?類之外的代碼,嘗試new的時候,勢必就要調用構造方法,由于構造方法私有,無法調動,就會編譯出錯
3.兩種寫法
(1)餓漢模式(早創建)
唯一實例創建時機非常早,類似于餓了很久的人,?看到了吃的就趕緊開始吃(急迫)
package thread;//單例,餓漢模式
//唯一實例創建時機非常早,類似于餓了很久的人,看到了吃的就趕緊開始吃(急迫)
class Singleton {private static Singleton instance = new Singleton();public static Singleton getInstance() {return instance;}private Singleton() {}
}
public class Demo27 {public static void main(String[] args) {Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1 == s2);//Singleton s3 = new Singleton();}
}
?(2)懶漢模式(緩執行,可能不執行)
懶是提高效率,節省開銷的體現 啥時候調用,就啥時候創建,如果不調用,就不創建了
package thread;//單例模式,懶漢模式的實現
class SingletonLazy {private static SingletonLazy instance = null;public static SingletonLazy getInstance() {if (instance == null) {instance = new SingletonLazy();}return instance;}private SingletonLazy (){}
}public class Demo28 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);//SingletonLazy s3 = new SingletonLazy();}
}
Q:如果有多個線程同時調用getInstance,是否會產生線程安全問題?
A:
- 餓漢模式不存在線程安全問題,而懶漢模式存在
? - 餓漢模式:實例早就有了,每個線程getInstance,就是讀取上面的靜態變量,多個線程讀取同一個變量,是線程安全的
- 懶漢模式:instance = new SingletonLazy,賦值操作就是修改,而且操作不是原子的,肯定就是線程不安全的
圖解:懶漢模式:非原子性操作
- 還可能存在其他執行順序,t2再次new,可能創建多個實例
- instance只是一個引用,instance中地址指向的那個對象可能就是一個大對象,上述代碼會出現覆蓋,第二個對象的地址覆蓋了第一個,進一步第一個對象沒有引用指向了,就會被GC回收(但是這個創建時間的開銷,是客觀存在的)
4.應用場景
(1)寫的服務器,要從硬盤上加載100G的數據到內存(加載到若干個哈希表中),肯定要寫一個類,封裝上述加載操作,并且獲取一些獲取、處理數據的業務邏輯
- 代碼中的有些對象,本身就不應該是多個實例的,從業務角度就應該是單個實例
一個實例就管理100G的內存數據 - 搞多個實例,就是N*100G的內存數據,機器肯定吃不消,沒必要
(2)MySQL的配置文件,專門管理配置,需要加載配置數據到內存中提供其他代碼使用,這樣的類也是單例的
- 如果是多個實例,就存儲了多份數據,如果一樣還罷了,如果不一樣,以哪個為準?
🔥5.多線程中的單例模式
懶漢模式的線程安全問題如何解決呢?
(1)加鎖
Q:這樣加鎖線程就安全了嗎?
public static SingletonLazy getInstance() {if (instance == null) {synchronized (locker) {instance = new SingletonLazy();}}return instance;}
A:?沒有,t2拿到鎖后,還在直接執行new操作
圖解:
應該把if和new操作打包成一個原子操作
public static SingletonLazy getInstance() {synchronized (locker) {if(instance == null) {instance = new SingletonLazy();}}return instance;}
(2)雙重if
?加鎖之后的問題Q:懶漢模式只有最開始調用getInstance會存在線程安全問題,一旦把實例創建好了,后續再調用,就是只讀操作,就不存在線程安全問題了,但是上述代碼,只要一調用getInstance方法,就需要先加鎖,再執行后續操作(后續沒有線程安全問題,還要加鎖,有開銷)
真正的解決A:再加一層判斷
連續兩次一樣的條件判斷(單線程中無意義,但是多線程含義就不一樣)
第一層if:判定是否要加鎖(new之前要加鎖,new之后就不用加了)
第二層if:判斷是否要創建對象
?指令級理解
?t2拿到鎖,這個時候instance已經被t1修改了
(3)volatie
private static volatile SingletonLazy instance = null;
為了保證第一次線程修改,后續線程一定會讀到,加上volatile【避免內存可見性+指令重排序問題】
綜上:懶漢模式的線程安全版
//單例模式,懶漢模式的實現
class SingletonLazy {private static volatile SingletonLazy instance = null;private static Object locker = new Object();public static SingletonLazy getInstance() {if (instance == null) {synchronized (locker) {if(instance == null) {instance = new SingletonLazy();}}}return instance;}private SingletonLazy (){}
}
6.指令重排序(小概率出現問題)
編譯器的優化策略有很多: ?
把讀內存優化到讀寄存器,指令重排序,循環展開,條件分支預測...
(1)什么是指令重排序?
編譯器會在保證邏輯是等價的情況下,調整二進制指令的執行順序,從而提高效率
- 正常來說寫的代碼,最終會編譯成為一系列的二進制指令,CPU會按照順序,一條一條執行
?(2)以Instance = new SingletonLazy( );為例分析
- 這行代碼可以細分為三個步驟
- 申請內存空間
- 調用構造方法(對內存空間進行優化)
- 把此時內存空間的地址,賦值給Instance引用
- 在指令重排序的策略下,上述執行的過程,不一定是123,也可能是132(但是1一定先執行)
- 132這樣的執行順序,就可能存在線程安全問題
- 指令級理解
- 3一旦執行完,就意味著instance就非null,但是指向的對象其實是一個未初始化的對象(里面的成員都是0)
- 執行到t2的時候,instance已經非null了,這里的條件無法進行,直接返回未初始完畢的instance
- 后續如果t2中還有其他邏輯,就會對未初始完畢的對象進行操作,這樣存在嚴重的問題
(3)解決上述指令重排序問題
- 加上volatile,主要是針對某個對象的讀寫過程中,不會出現重排序
- 很多地方都能重排序,但是只是針對這一過程中
- 這樣t2線程讀到的數據,一定是t1已經構造完畢的完整對象了(一定是123執行的對象)
7.延伸:了解
(1)單例模式要確保反射下安全,即使動用反射也無法破壞單例特性
(2)單例模式要確保序列化下安全,即使動用Java標準庫的序列化機制,也無法破壞單例特性
- enum類型的實例天然支持序列化和反序列化
- 序列化:把對象轉為二進制字符串
🔥8.常見考察
(1)為什么說餓漢式單例天生就是線程安全的?
實例早就有了,每個線程getInstance,就是讀取上面的靜態變量,多個線程讀取同一個變量,是線程安全的
(2)傳統的懶漢式單例為什么是非線程安全的?
instance = new SingletonLazy,賦值操作就是修改,而且操作不是原子的,肯定就是線程不安全的
(3)怎么修改傳統的懶漢式單例,使其線程變得安全?
1.線程不安全的版本
2.加鎖版本
3.加上雙重if
4.最后加上volatile
(4)線程安全的單例的實現還有哪些,怎么實現?
靜態內部類單例
public class StaticInnerClassSingleton {private StaticInnerClassSingleton() {}private static class SingletonHolder {private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();}public static StaticInnerClassSingleton getInstance() {return SingletonHolder.INSTANCE;} }
?靜態內部類在外部類加載時不會被加載,只有在調用?getInstance?方法時才會加載類,從而創建實例。類加載過程是線程安全的,所以這種方式實現了線程安全的單例,并且具有延遲加載的特性
(5)雙重檢查模式、Volatile關鍵字在單例模式中的應用
1.雙重檢查模式:
第一層if:判定是否要加鎖(new之前要加鎖,new之后就不用加了)
第二層if:判斷是否要創建對象
2.Volatile關鍵字:避免內存可見性+指令重排序問題
(6)ThreadLocal在單例模式中的應用
public class ThreadLocalSingleton {private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() {@Overrideprotected ThreadLocalSingleton initialValue() {return new ThreadLocalSingleton();}};private ThreadLocalSingleton() {}public static ThreadLocalSingleton getInstance() {return threadLocalInstance.get();}
}
ThreadLocal 會為每個使用該實例的線程都提供一個獨立的副本,每個線程都可以獨立地改變自己的副本,而不會影響其他線程所對應的副本。在單例模式中使用 ThreadLocal,可以保證每個線程都有自己的單例實例,適用于需要在每個線程中維護單例狀態的場景?
(7)枚舉式單例
public enum EnumSingleton {INSTANCE;public void doSomething() {System.out.println("Doing something...");}
}
?枚舉式單例是實現單例模式的最佳方式之一。它是線程安全的,因為枚舉類型的實例創建是由 JVM 保證線程安全的。而且枚舉類型可以防止反序列化和反射攻擊,因為 Java 規范中規定,枚舉類型的 clone()、readObject()、readResolve() 等方法都不會破壞單例的唯一性。使用時可以直接通過 EnumSingleton.INSTANCE 來獲取單例實例