DCL單例模式


  我們第一次寫的單例模式是下面這樣的:

 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的用法在此不展開,之后會另行介紹。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM