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


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

 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關鍵字:

 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處進行同步:

 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就不再為空了,但是后續的線程並沒有做第二次非空檢查。那么很明顯,在同步代碼塊中應該再次做檢查,也就是所謂的雙重檢測:

④雙重檢測:

 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關鍵字解析


免責聲明!

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



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