通過單例模式理解synchronized,volatile

synchronized 關鍵字

synchronized關鍵字是用來控制線程同步的,就是在多線程的環境下,控制synchronized代碼段不被多個線程同時執行。是一種阻塞性的鎖,synchronized既可以加在一段代碼上,也可以加在方法上。
synchronized(this)及非static的synchronized方法,只能防止多個線程同時執行同一個對象的同步代碼段。當synchronized鎖住一個對象后,別的線程如果也想拿到這個對象的鎖,就必須等待這個線程執行完成釋放鎖,才能再次給對象加鎖,這樣才達到線程同步的目的。即使兩個不同的代碼段,都要鎖同一個對象,那么這兩個代碼段也不能在多線程環境下同時運行。
所以我們在用synchronized關鍵字的時候,能縮小代碼段的范圍就盡量縮小,能在代碼段上加同步就不要再整個方法上加同步。這叫減小鎖的粒度,使代碼更大程度的并發。原因是基于以上的思想,鎖的代碼段太長了,別的線程是不是要等很久。
如果用synchronized加在靜態方法上,就相當于用××××.class鎖住整個方法內的代碼塊,此時是鎖住該類的Class對象,相當于一個全局,鎖。使用synchronized修飾的方法或者代碼塊可以看成是一個原子操作。
一個線程執行互斥代碼過程如下:
1. 獲得同步鎖;
2. 清空工作內存;
3. 從主內存拷貝對象副本到工作內存;
4. 執行代碼(計算或者輸出等);
5. 刷新主內存數據;
6. 釋放同步鎖。
所以,synchronized既保證了多線程的并發有序性,又保證了多線程的內存可見性。

volatile(非阻塞性的)

volatile的特性當我們聲明共享變量為volatile后,對這個變量的讀/寫將會很特別。理解volatile特性的一個好方法是:把對volatile變量的單個讀/寫,看成是使用同一個監視器鎖對這些單個讀/寫操作做了同步。

它的工作原理是,它對寫和讀都是直接操作工作主存的。下面我們通過具體的示例來說明,請看下面的示例代碼:

class VolatileFeaturesExample {  
    volatile long vl = 0L;  //使用volatile聲明64位的long型變量        
    public void set(long l) {            
        vl = l;   //單個volatile變量的寫        
    }            
    public void getAndIncrement () { 
        vl++;   //復合(多個)volatile變量的讀/寫            
    }          
    public long get()  {            
        return vl;   //單個volatile變量的讀        
    }    
}
class VolatileFeaturesExample {      
    volatile long vl = 0L;  //使用volatile聲明64位的long型變量        
    public void set(long l) {           
        vl = l;   //單個volatile變量的寫        
    }            
    public void getAndIncrement () {                         
        vl++;    //復合(多個)volatile變量的讀/寫        
    }                
    public long get() {            
        return vl;   //單個volatile變量的讀     
    }  
}

假設有多個線程分別調用上面程序的三個方法,這個程序在語意上和下面程序等價:

 class VolatileFeaturesExample {       
       long vl = 0L;               // 64位的long型普通變量        
       //對單個的普通 變量的寫用同一個監視器同步          
       public synchronized void set(long l) {             
          vl = l;       
       }           
       public void getAndIncrement () { 
        //普通方法調用            
        long temp = get();           //調用已同步的讀方法           
        temp += 1L;                  //普通寫操作                  
        set(temp);                   //調用已同步的寫方法        
      }        
      public synchronized long get() {        
      //對單個的普通變量的讀用同一個監視器同步                
          return vl;      
      }    
}

臨界區代碼的執行具有原子性。這意味著即使是64位的long型和double型變量,只要它是volatile變量,對該變量的讀寫就將具有原子性。
如果是多個volatile操作或類似于volatile++這種復合操作,這些操作整體上不具有原子性。簡而言之,volatile變量自身具有下列特性:

  • 可見性: 對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
  • 對 volatile 變量的寫操作,不允許和它之前的讀寫操作打亂順序;對 volatile 變量的讀操作,不允許和它之后的讀寫亂序。

volatile不能保證原子性,原子性不是volatile來保證的,如果操作本來就具有原子性,volatile就會保證這個原子性不會被打破,理解為加上同步。比如上面例子中的get和set函數。
以及下面這段線程不安全的singleton(單例模式)實現,盡管使用了volatile:

public class UnSafeSingleton {        
    private static volatile UnSafeSingleton       
    sInstance = null;              
    private UnSafeSingleton() {}             
    public static UnSafeSingleton getInstance(){
        if (sInstance == null) {                     
            sInstance = new UnSafeSingleton();                
        }            
        return sInstance;       
     }    
}

關于原子性的理解:只對單個操作就有原子性,比如i++這種就不是單個操作。

但是,輕易不要用volatile來替代synchronized來避免并發問題,因為很多原子操作自己可能并沒那么了解,除非你是并發專家。《java編程思想》的作者是這樣建議的。

單例模式的應用

先看一個"餓漢式"的模式

public class Singleton {      
    private static Singleton instance = new Singleton();     
    private Singleton (){}      
    public static Singleton getInstance() {            
         return instance;     
    }  
}

這種方式比較常用,但容易產生垃圾對象。
優點:沒有加鎖,執行效率會提高。
缺點:類加載時就初始化,浪費內存。
它基于 Classloader 機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態方法,比如反射)導致類裝載,這時候初始化 instance顯然沒有達到 lazy loading 的效果。
一個雙重鎖定式的單例模式

public class Singleton {      
    private volatile static Singleton sSingleton;              
    private Singleton (){}      
    public static Singleton getSingleton() {      
        if (sSingleton == null) {                   
            synchronized (Singleton.class) {                    
                if (sSingleton == null) {                                       
                    sSingleton = new Singleton();                  
                }          
            }     
         }      
         return sSingleton;      
    }  
} 

討論下volatile關鍵字的必要性,如果沒有volatile關鍵字,問題可能會出在singleton = new Singleton();這句,用偽代碼表示

inst = allocat(); // 分配內存  
sSingleton = inst;      // 賦值
constructor(inst); // 真正執行構造函數  

可能會由于虛擬機的優化等導致賦值操作先執行,而構造函數還沒完成,導致其他線程訪問得到singleton變量不為null,但初始化還未完成,導致程序崩潰。
下篇你見過這樣的單例模式嗎將見識到一種新的單例模式。

參考:Java 單例真的寫對了么?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區別 13、...
    Miley_MOJIE閱讀 3,731評論 0 11
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,366評論 11 349
  • 一個簡單的單例示例 單例模式可能是大家經常接觸和使用的一個設計模式,你可能會這么寫 publicclassUnsa...
    Martin說閱讀 2,264評論 0 6
  • 前幾年我在成都給美國人教漢語的時候,有好多時候都差點把我逼瘋了,對!把我逼瘋了! 他們問我的問題我答不上來,可我是...
    北冥有路閱讀 861評論 5 10
  • 我去年離開濟南的時候,房價已經漲的特別離譜了,我工作的地方在濟南東邊比較偏的位置,周圍大多是民房和安置小區。一起做...
    哈里的魔法世界閱讀 267評論 0 0