目錄
- 爲什麼需要單例
- 餓漢模式的簡單實現
- 懶漢模式的簡單實現
- 二者比較
- 解決懶漢模式的線程不安全問題
爲什麼需要單例
單例模式能夠保證一個類僅有唯一的實例,並提供一個全局訪問點。
我們是不是可以通過一個全局變量來實現單例模式的要求呢?我們只要仔細地想想看,全局變量確實可以提供一個全局訪問點,但是它不能防止別人實例化多個對象。通過外部程序來控制的對象的產生的個數,勢必會系統的增加管理成本,增大模塊之間的耦合度。所以,最好的解決辦法就是讓類自己負責保存它的唯一實例,並且讓這個類保證不會產生第二個實例,同時提供一個讓外部對象訪問該實例的方法。自己的事情自己辦,而不是由別人代辦,這非常符合面向對象的封裝原則。
單例模式主要有3個特點:
- 單例類確保自己只有一個實例。
- 單例類必須自己創建自己的實例。
- 單例類必須爲其他對象提供唯一的實例。
餓漢模式
實現單例的餓漢模式主要有3步:
- 讓默認的構造函數私有化,使外部類無法通過new的方式獲取該類的實例
private SingletonHunger() {}
- 我們依舊要提供實例給外部,而外部類又無法取得該類的實例對象,所以我們將實例以靜態變量的方式提供
public static SingletonHunger instance = new SingletonHunger();
public static void main(String[] args) {
SingletonHunger s1 = SingletonHunger.instance;
SingletonHunger s2 = SingletonHunger.instance;
System.out.println(s1 == s2); // true
}
- 將instance類變量私有化並提供getter訪問器
private static SingletonHunger instance = new SingletonHunger();
public static SingletonHunger getInstance() {
return instance;
}
餓漢模式有什麼特點呢,可以看到最明顯的就是instance是靜態成員變量,它在類被加載的時候就會被實例化,不管有沒有被其它外部類訪問,所以這種一開始就實例化好的,我們覺得它很着急,所以叫餓漢模式。
public class SingletonHunger {
// 1. 將默認的構造函數私有化
private SingletonHunger() {}
// 2. 提供靜態變量
private static SingletonHunger instance = new SingletonHunger();
// 3. 創建instance變量的getter訪問器
public static SingletonHunger getInstance() {
return instance;
}
}
懶漢模式
懶漢模式和餓漢模式的卻別就在於懶漢模式只是聲明類的實例變量,而在有外部類(線程)訪問的時候纔去真正實例化(開闢內存空間),之後的所有線程都共享最先創建的那個實例
public class SingletonLazy {
// 1. 將默認的構造函數私有化
private SingletonLazy() {
}
// 2. 聲明類的唯一實例 只是聲明
private static SingletonLazy instance;
// 3. 爲instance提供訪問器
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
二者比較
餓漢模式:類加載時較慢,訪問對象時較快,線程安全
懶漢模式:類加載時較快,訪問對象時較慢,線程不安全
大多數應用場景下都使用懶漢模式,因爲餓漢模式還有一個問題那便是內存空間的浪費。所以我們就需要解決懶漢模式的線程不安全問題
解決懶漢模式的線程不安全問題
主要解決的問題是兩個
- 線程的併發
- JVM的指令重排
第一個問題我們可以用加鎖來實現,用sychronizd即可,第二個問題是Java的關鍵字volatile。
**加鎖:**我們可以直接加在getter上,但是這樣子的靜態方法鎖整個臨界區比較大,比較耗費資源,所以使用同步代碼塊
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
爲什麼要判空兩次?其實就是爲了用同步代碼塊,你必須保證臨界區完成一整套必不可少的操作,最開始的判空只是判斷是否需要進入臨界區。加入兩個線程同時停在了第一個判空處,其中一個線程獲得鎖進去不判空直接new,那麼它完成操作釋放鎖之後對於第二個等待鎖的線程而言,它獲得一釋放的鎖之後也是進去直接new,很顯然,這一點都不符合臨界區的設計。
volatile
instance = new Singleton();
這條語句並不是一個原子操作
- 分配內存給對象,在內存中開闢一段地址空間;// Singleton var = new Singleton();
- 對象的初始化;// var = init();
- 將分配好對象的地址指向instance變量,即instance變量的寫;// instance = var;
設想一下,如果不使用volatile關鍵字限制指令的重排序,1-3-2操作,獲得到單例的線程可能拿到了一個空對象,後續操作會有影響!因此需要引入volatile對變量進行修飾。
再詳細的內容參考博客
所以最後我們得到的結果爲這樣
public class SingletonLazy {
// 1. 將默認的構造函數私有化
private SingletonLazy() {
}
// 2. 聲明類的唯一實例 只是聲明
private volatile static SingletonLazy instance;
// 3. 爲instance提供訪問器
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
參考博客
java單例設計模式詳解(懶漢餓漢式)+深入分析爲什麼懶漢式是線程不安全的+解決辦法:https://blog.csdn.net/yaoyaoyao_123/article/details/84799861
【JAVA】線程安全的懶漢模式爲什麼要使用volatile關鍵字:https://blog.csdn.net/weixin_42078452/article/details/84892372