雙重檢查鎖的漏洞 -- volatile 指令進行重排
public class SimpleSingleton7 { private volatile static SimpleSingleton7 INSTANCE; private SimpleSingleton7() { } public static SimpleSingleton7 getInstance() { if (INSTANCE == null) { synchronized (SimpleSingleton7.class) { if (INSTANCE == null) { INSTANCE = new SimpleSingleton7(); } } } return INSTANCE; } }
如果不新增 volatitle, java虛擬機器實際上會做一些優化,對一些程式碼指令進行重排。 變成:
public static SimpleSingleton4 getInstance() { if (INSTANCE == null) {//1 if (INSTANCE == null) {//3 synchronized (SimpleSingleton4.class) {//2 INSTANCE = new SimpleSingleton4();//4 } } } return INSTANCE;//5 }
詳細:
........................................
單例模式有:餓漢模式
和懶漢模式
兩種。
餓漢模式程式碼如下:
public class SimpleSingleton { //持有自己類的引用 private static final SimpleSingleton INSTANCE = new SimpleSingleton();//私有的構造方法 private SimpleSingleton() { } //對外提供獲取例項的靜態方法 public static SimpleSingleton getInstance() { return INSTANCE; } }
使用餓漢模式的好處是:沒有執行緒安全的問題,但帶來的壞處也很明顯。
private static final SimpleSingleton INSTANCE = new SimpleSingleton();
一開始就例項化物件了,如果例項化過程非常耗時,並且最後這個物件沒有被使用,不是白白造成資源浪費嗎?
還真是啊。
這個時候你也許會想到,不用提前例項化物件,在真正使用的時候再例項化不就可以了?
這就是我接下來要介紹的:懶漢模式
。
具體程式碼如下:
public class SimpleSingleton2 { private static SimpleSingleton2 INSTANCE; private SimpleSingleton2() { } public static SimpleSingleton2 getInstance() { if (INSTANCE == null) { INSTANCE = new SimpleSingleton2(); } return INSTANCE; } }
示例中的INSTANCE物件一開始是空的,在呼叫getInstance方法才會真正例項化。
嗯,不錯不錯。但這段程式碼還是有問題。
假如有多個執行緒中都呼叫了getInstance方法,那麼都走到 if (INSTANCE == null) 判斷時,可能同時成立,因為INSTANCE初始化時預設值是null。這樣會導致多個執行緒中同時建立INSTANCE物件,即INSTANCE物件被建立了多次,違背了只建立一個INSTANCE物件的初衷。
為了解決餓漢模式
和懶漢模式
各自的問題,於是出現了:雙重檢查鎖
。
具體程式碼如下:
public class SimpleSingleton4 { private static SimpleSingleton4 INSTANCE; private SimpleSingleton4() { } public static SimpleSingleton4 getInstance() { if (INSTANCE == null) { synchronized (SimpleSingleton4.class) { if (INSTANCE == null) { INSTANCE = new SimpleSingleton4(); } } } return INSTANCE; } }
需要在synchronized
前後兩次判空。
但我要告訴你的是:這段程式碼有漏洞的。
有什麼問題?
public static SimpleSingleton4 getInstance() { if (INSTANCE == null) {//1 synchronized (SimpleSingleton4.class) {//2 if (INSTANCE == null) {//3 INSTANCE = new SimpleSingleton4();//4 } } } return INSTANCE;//5 }
getInstance方法的這段程式碼,我是按1、2、3、4、5這種順序寫的,希望也按這個順序執行。
但是java虛擬機器實際上會做一些優化,對一些程式碼指令進行重排。重排之後的順序可能就變成了:1、3、2、4、5,這樣在多執行緒的情況下同樣會建立多次例項。重排之後的程式碼可能如下:
public static SimpleSingleton4 getInstance() { if (INSTANCE == null) {//1 if (INSTANCE == null) {//3 synchronized (SimpleSingleton4.class) {//2 INSTANCE = new SimpleSingleton4();//4 } } } return INSTANCE;//5 }
原來如此,那有什麼辦法可以解決呢?
答:可以在定義INSTANCE是加上volatile
關鍵字。具體程式碼如下:
public class SimpleSingleton7 { private volatile static SimpleSingleton7 INSTANCE; private SimpleSingleton7() { } public static SimpleSingleton7 getInstance() { if (INSTANCE == null) { synchronized (SimpleSingleton7.class) { if (INSTANCE == null) { INSTANCE = new SimpleSingleton7(); } } } return INSTANCE; } }
volatile
關鍵字可以保證多個執行緒的可見性
,但是不能保證原子性
。同時它也能禁止指令重排
。
雙重檢查鎖的機制既保證了執行緒安全,又比直接上鎖提高了執行效率,還節省了記憶體空間。