我們第一次寫的單例模式是下面這樣的:
1 public class Singleton { 2 private static Singleton instance = null; 3 public static Singleton getInstance() { 4 if(null == instance) { // line A 5 instance = new Singleton(); // line B 6 } 7 8 return instance; 9 10 } 11 }
假設這樣的場景:兩個線程並發調用Singleton.getInstance(),假設線程一先判斷instance是否為null,即代碼中line A進入到line B的位置。剛剛判斷完畢后,JVM將CPU資源切換給線程二,由於線程一還沒執行line B,所以instance仍然為空,因此線程二執行了new Singleton()操作。片刻之后,線程一被重新喚醒,它執行的仍然是new Singleton()操作,這樣問題就來了,new出了兩個instance,這還能叫單例嗎?
緊接着,我們再做單例模式的第二次嘗試:
1 public class Singleton { 2 private static Singleton instance = null; 3 public synchronized static Singleton getInstance() { 4 if(null == instance) { 5 instance = new Singleton(); 6 } 7 8 return instance; 9 10 } 11 }
比起第一段代碼僅僅在方法中多了一個synchronized修飾符,現在可以保證不會出線程問題了。但是這里有個很大(至少耗時比例上很大)的性能問題。除了第一次調用時是執行了Singleton的構造函數之外,以后的每一次調用都是直接返回instance對象。返回對象這個操作耗時是很小的,絕大部分的耗時都用在synchronized修飾符的同步准備上,因此從性能上來說很不划算。
繼續把代碼改成下面這樣:
1 public class Singleton { 2 private static Singleton instance = null; 3 public static Singleton getInstance() { 4 synchronized (Singleton.class) { 5 if(null == instance) { 6 instance = new Singleton(); 7 } 8 } 9 10 return instance; 11 12 } 13 }
基本上,把synchronized移動到代碼內部是沒有什么意義的,每次調用getInstance()還是要進行同步。同步本身沒有問題,但是我們只希望在第一次創建instance實例的時候進行同步,因此有了下面的寫法——雙重鎖定檢查(DCL,Double Check Lock)。
1 public class Singleton { 2 private static Singleton instance = null; 3 public static Singleton getInstance() { 4 if(null == instance) { // 線程二檢測到instance不為空 5 synchronized (Singleton.class) { 6 if(null == instance) { 7 instance = new Singleton(); // 線程一被指令重排,先執行了賦值,但還沒執行完構造函數(即未完成初始化) 8 } 9 } 10 } 11 12 return instance; // 后面線程二執行時將引發:對象尚未初始化錯誤 13 14 } 15 }
看樣子已經達到了要求,除了第一次創建對象之外,其它的訪問在第一個if中就返回了,因此不會走到同步塊中,已經完美了嗎?
如上代碼段中的注釋:假設線程一執行到instance = new Singleton()這句,這里看起來是一句話,但實際上其被編譯后在JVM執行的對應會變代碼就發現,這句話被編譯成8條匯編指令,大致做了三件事情:
1)給instance實例分配內存;
2)初始化instance的構造器;
3)將instance對象指向分配的內存空間(注意到這步時instance就非null了)
如果指令按照順序執行倒也無妨,但JVM為了優化指令,提高程序運行效率,允許指令重排序。如此,在程序真正運行時以上指令執行順序可能是這樣的:
a)給instance實例分配內存;
b)將instance對象指向分配的內存空間;
c)初始化instance的構造器;
這時候,當線程一執行b)完畢,在執行c)之前,被切換到線程二上,這時候instance判斷為非空,此時線程二直接來到return instance語句,拿走instance然后使用,接着就順理成章地報錯(對象尚未初始化)。
具體來說就是synchronized雖然保證了線程的原子性(即synchronized塊中的語句要么全部執行,要么一條也不執行),但單條語句編譯后形成的指令並不是一個原子操作(即可能該條語句的部分指令未得到執行,就被切換到另一個線程了)。
根據以上分析可知,解決這個問題的方法是:禁止指令重排序優化,即使用volatile變量。
1 public class Singleton { 2 private volatile static Singleton instance = null; 3 public static Singleton getInstance() { 4 if(null == instance) { 5 synchronized (Singleton.class) { 6 if(null == instance) { 7 instance = new Singleton(); 8 } 9 } 10 } 11 12 return instance; 13 14 } 15 }
將變量instance使用volatile修飾即可實現單例模式的線程安全。
關於volatile的用法在此不展開,之后會另行介紹。