Java併發筆記——單例與雙重檢測

       單例模式可使得一個類只有一個對象實例,可以減小頻繁建立對象的時間和空間開銷。單線程模式下一個典型的單例模式代碼以下:html

編程

 1 class Singleton{
 2     private static Singleton singleton;    
 3     private Singleton(){}
 4     
 5     public static Singleton getInstance(){
 6         if(singleton == null){
 7             singleton = new Singleton();   //1
 8         }
 9         return singleton;
10     }
11 }

       構造器私有使得外界沒法經過構造器實例化Singleton類,要取得實例只能經過getInstance()方法。這是一個延遲加載的版本,即在須要對象的時候才進行實例化操做。該方法在單線程下可以正常運行,可是在多線程環境下會出現因爲沒有同步措施而致使產生多個單例對象的狀況。緣由在於可能同時有兩個線程A和B同時執行到 if 條件判斷語句,A判斷singleton爲空準備執行//1時讓出了CPU時間片,B也判斷singleton爲空,接着執行//1,此時建立了一個實例對象;A獲取了CPU時間片後接着執行//1,也建立了實例對象,這就致使多個單例對象的狀況。多線程

       解決問題的方法也很簡單,使用synchronized關鍵字:併發

spa

 1 class Singleton{
 2     private static Singleton singleton;    
 3     private Singleton(){}
 4     
 5     public static synchronized Singleton getInstance(){
 6         if(singleton == null){
 7             singleton = new Singleton();    //1
 8         }
 9         return singleton;
10     }
11 }

        這樣解決了多線程併發的問題,可是卻帶來了效率問題:咱們的目的是隻建立一個實例,即//1處代碼只會執行一次,也正是這個地方纔須要同步,後面建立了實例以後,singleton非空就會直接返回對象引用,而不用每次都在同步代碼塊中進行非空驗證。那麼能夠考慮只對//1處進行同步:.net

線程

 1 class Singleton{
 2     private static Singleton singleton;    
 3     private Singleton(){}
 4     
 5     public static Singleton getInstance(){
 6         if(singleton == null){
 7             synchronized(Singleton.class){                
 8                 singleton = new Singleton();   //1
 9             }
10         }
11         return singleton;
12     }
13 }

       這樣會帶來與第一種同樣的問題,即多個線程同時執行到條件判斷語句時,會建立多個實例。問題在於當一個線程建立一個實例以後,singleton就再也不爲空了,可是後續的線程並無作第二次非空檢查。那麼很明顯,在同步代碼塊中應該再次作檢查,也就是所謂的雙重檢測:code

④雙重檢測:htm

 1 class Singleton{
 2     private static Singleton singleton;    
 3     private Singleton(){}
 4     
 5     public static Singleton getInstance(){
 6         if(singleton == null){
 7             synchronized(Singleton.class){
 8                 if(singleton == null)
 9                     singleton = new Singleton();   //1
10             }
11         }
12         return singleton;
13     }
14 }

       到這裏已經很完美了,看起來沒有問題。可是這種雙重檢測機制在JDK1.5以前是有問題的,問題仍是出在//1,由所謂的無序寫入形成的。通常來說,當初始化一個對象的時候,會經歷內存分配、初始化、返回對象在堆上的引用等一系列操做,這種方式產生的對象是一個完整的對象,能夠正常使用。可是JAVA的無序寫入可能會形成順序的顛倒,即內存分配、返回對象引用、初始化的順序,這種狀況下對應到//1就是singleton已經不是null,而是指向了堆上的一個對象,可是該對象卻尚未完成初始化動做。當後續的線程發現singleton不是null而直接使用的時候,就會出現意料以外的問題。對象

        JDK1.5以後,可使用volatile關鍵字修飾變量來解決無序寫入產生的問題,由於volatile關鍵字的一個重要做用是禁止指令重排序,即保證不會出現內存分配、返回對象引用、初始化這樣的順序,從而使得雙重檢測真正發揮做用。

       固然,也能夠選擇不使用雙重檢測,而採用非延遲加載的方式來達到相同的效果:

1 class Singleton{
2     private static Singleton singleton = new Singleton();    
3     private Singleton(){}
4     
5     public static Singleton getInstance(){
6         return singleton;
7     }
8 }

【參考】

Java單例模式中雙重檢查鎖的問題

單例模式與雙重檢測

Java 中的雙重檢查(Double-Check)

Java併發編程:volatile關鍵字解析

相關文章
相關標籤/搜索