咱們第一次寫的單例模式是下面這樣的:安全
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)。spa
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條彙編指令,大體作了三件事情:code
1)給instance實例分配內存;對象
2)初始化instance的構造器;blog
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的用法在此不展開,以後會另行介紹。