我的GitHub | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|
baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
簡介
作用:保證類只有一個實例;提供一個全局訪問點
JDK中的案例:
java.lang.Runtime#getRuntime()
單例模式的幾種方式
餓漢式:簡單安全,但浪費資源
- 優點:以空間換時間。因為靜態變量會在類加載時初始化,此時不會涉及多個線程對象訪問該對象的問題,虛擬機保證只會加載一次該類,肯定不會發生並發訪問的問題,因此,可以省略
synchronized
關鍵字。 - 問題:如果只是加載本類,而不調用
getInstance()
,甚至永遠都不調用,則會造成資源浪費
!
class Single {
private static Single SINGLETON = new Single();//類加載的時候會連帶着創建實例
private Single() {
System.out.println("創建了實例");
}
public static Single getInstance() {
return SINGLETON;
}
}
懶漢式
簡單懶漢式:高效,但不安全
- 優點:以時間換空間。延時加載,懶加載,用的時候才加載,不會像餓漢式那樣可能造成資源浪費!
- 問題:在多線程環境下存在風險
class Single {
private static Single SINGLETON;
private Single() {
System.out.println("創建了實例");
}
public static Single getInstance() {
if (SINGLETON == null) SINGLETON = new Single();
return SINGLETON;
}
}
線程安全問題出現場景
如果兩個線程A和B同時調用了該方法,然后以如下方式執行:
- A進入if判斷,此時 instance 為 null,因此進入if內
- B進入if判斷,此時A還沒有創建 instance,因此 instance 也為 null,因此B也進入if內
- A創建了一個instance並返回
- 雖然此時 instance 不為 null,但因為B已經進入了if判斷,所以B也會創建一個instance並返回
- 此時問題出現了,我們的單例被創建了兩次!
驗證測試代碼
驗證邏輯很簡單,首先在單例的構造方法
中打印一條日志,然后我們創建幾十上百個線程,並發的去調用單例模式的getInstance()
方法,通過日志打印來判斷到底執行了幾次構造方法,依次來驗證此種單例模式是否是安全的。
public class Test {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
Single.getInstance();
}
};
for (int i = 0; i < 100; i++) {
new Thread(runnable).start();
}
}
}
經過測試,這種場景下多次創建Single實例並非是小概率事件,反而是大概率事件!
加同步鎖方式:性能差
以上問題最直觀的解決辦法就是給getInstance
方法加上一個synchronize
鎖,這樣每次只允許一個線程調用getInstance方法:
class Single {
private static Single SINGLETON;
private Single() {
System.out.println("創建了實例");
}
public static synchronized Single getInstance() {
if (SINGLETON == null) SINGLETON = new Single();
return SINGLETON;
}
}
這種解決辦法的確可以防止錯誤的出現,但是它卻很影響性能:每次調用getInstance
方法的時候都必須獲得Singleton.class
鎖!
而實際上,僅僅在創建實例時有線程安全問題,而當單例實例被創建以后,其后的請求沒有必要再使用互斥機制了。
☆ 雙重檢查加鎖模式:高效且安全
基本形式
目前大量人使用的都是這個double-checked locking
的解決方案:
class Single {
private static Single SINGLETON; //沒添加【volatile】關鍵字之前
private Single() {
System.out.println("創建了實例");
}
public static Single getInstance() {
if (SINGLETON == null) { //目的是為了提高性能,避免非必要加鎖
synchronized (Single.class) { //加鎖保證現場安全
if (SINGLETON == null) SINGLETON = new Single(); //避免重復創建實例
}
}
return SINGLETON;
}
}
讓我們來看一下這個代碼是如何工作的:
- 當一個線程調用
getInstance()
方法后,首先會先檢查SINGLETON是否為null,如果不是則直接返回其內容,這樣避免了進入synchronized塊所需要花費的資源。 - 其次,即使上面提到的情況發生了,即兩個線程同時進入了第一個if判斷,那么他們也必須按照順序執行
synchronized
塊中的代碼,第一個進入代碼塊的線程會創建一個新的Single實例,而后續的線程則因為無法通過if判斷,而不會創建多余的實例。
存在的安全隱患:指令重排序
上述描述似乎已經解決了我們面臨的所有問題,但實際上,從JVM的角度講,這些代碼仍然可能發生錯誤。
對於JVM而言,它執行的是一個個Java指令。在Java指令中創建對象和賦值操作是分開進行的
,也就是說SINGLETON = new Single();
語句是分兩步執行的,但是JVM並不保證這兩個操作的先后順序,也就是說有可能JVM會先為新的Single實例分配空間,然后直接賦值給SINGLETON,然后再去初始化這個Single實例
。即先賦值指向了內存地址,再初始化
,這樣就使出錯成為了可能。
我們仍然以A、B兩個線程為例:
- A、B線程同時進入了第一個 if 判斷
- A首先進入
synchronized
塊,由於SINGLETON為null,所以它執行SINGLETON = new Single();
- 由於JVM內部的優化機制,JVM先划出了一些分配給Single實例的
空白內存
,並賦值給SINGLETON成員(注意此時JVM沒有開始初始化這個實例),然后A離開了synchronized塊。 - 然后B進入synchronized塊,由於SINGLETON此時不是null,因此它馬上離開了synchronized塊並將結果返回給調用該方法的程序。
- 此時B線程打算使用Single實例,
卻發現它沒有被初始化
,於是錯誤發生了。
下面參考另一篇文章的描述,意思是一樣的
問題主要在於SINGLETON = new Single();
這段代碼並不是原子操作,原子性:即一個操作或者多個操作,要么全部執行並且執行的過程不會被任何因素打斷,要么就都不執行(事務)。
SINGLETON = new Single();
其實做了三件事情:
- 給 SINGLETON 實例分配內存
- 初始化 Single() 實例
- 將SINGLETON對象指向 new Single() 分配的內存空間
問題就出在這兒了,因為JVM中有指令重排序的優化,所以呢正常情況按照1,2,3的順序來,沒毛病,但是也可能按照1,3,2的順序來,這個時候就有問題了,調用的時候判斷 SINGLETON != null
就直接返回 SINGLETON 實例,但是這個時候並沒有進行初始化工作,所以在后續的調用中肯定就會報錯了。
所以這里需要引入了 volatile 修飾符修飾 SINGLETON 對象,因為 volatile 能夠禁止指令重排序的功能,所以能解決我們的這個問題
以上情況只是理論分析,實際我經過大量測試發現,這種情況根本展示不出來。但是面試時這個是非常重要的知識點。
添加 volatile 后的終極形式
在JDK1.5
之后,官方也發現了這個問題,故而具體化了 volatile
,即在JDK1.6
及以后,只要定義為private volatile static
就可解決 DCL 失效問題。
volatile 確保 INSTANCE 每次均在主內存中讀取,這樣雖然會犧牲一點效率,但也無傷大雅。
class Single {
private static volatile Single SINGLETON; //添加【volatile】關鍵字
private Single() {
System.out.println("創建了實例");
}
public static Single getInstance() {
if (SINGLETON == null) { //目的是為了提高性能,避免非必要獲取鎖
synchronized (Single.class) { //加鎖
if (SINGLETON == null) SINGLETON = new Single(); //線程安全
}
}
return SINGLETON;
}
}
☆ 靜態內部類方式:【推薦】
JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證INSTANCE只被創建一次,並且會保證把賦值給INSTANCE的內存初始化完畢,這樣我們就不用擔心上面的問題。
另外,INSTANCE是在第一次加載SingleHolder時被創建的,而SingleHolder則只在調用getInstance方法的時候才會被加載,因此也實現了懶加載。
總結:不會像餓漢式那樣立即加載對象,且加載類時是線程安全的,從而兼具了並發高效調用和延遲加載
的優勢!
class Single {
private Single() {
System.out.println("創建了實例");
}
public static Single getInstance() { //只有調用getInstance時才會加載靜態內部類
return SingleHolder.SINGLETON;
}
private static class SingleHolder {
private static final Single SINGLETON = new Single();
}
}
這種方法不僅能確保線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。
枚舉式:天然的防止反射
線程安全、調用效率高,但不能延時加載,並且可以天然的防止反射和反序列化漏洞!
enum Single {
SINGLETON;//定義一個枚舉的元素,它就代表了Single的一個實例。元素的名字隨意。
Single() {
System.out.println("創建了實例");
}
}
2016-03-20