volatile的適用場景

介紹

把代碼塊聲明爲 synchronized,有兩個重要後果,一般是指該代碼具備 原子性(atomicity)可見性(visibility)java

  • 原子性意味着個時刻,只有一個線程可以執行一段代碼,這段代碼經過一個monitor object保護。從而防止多個線程在更新共享狀態時相互衝突。 所謂原子性操做是指不會被線程調度機子打斷的操做,這種操做一旦開始,就一直到幸運星結束,中間不會有任何切換(切換線程)。
  • 可見性則更爲微妙,它必須確保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的。 —— 若是沒有同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這將引起許多嚴重問題。

volatile的使用條件:編程

volatile變量具備 synchronized 的可見性特性,可是不具有原子性。這就是說線程可以自動發現 volatile 變量的最新值安全

volatile變量可用於提供線程安全,可是隻能應用於很是有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。所以,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具備與多個變量相關的不變式(Invariants)的類(例如 「start <=end」)。 多線程

出於簡易性或可伸縮性的考慮,您可能傾向於使用 volatile 變量而不是鎖。當使用 volatile 變量而非鎖時,某些習慣用法(idiom)更加易於編碼和閱讀。此外,volatile 變量不會像鎖那樣形成線程阻塞,所以也不多形成可伸縮性問題。在某些狀況下,若是讀操做遠遠大於寫操做,volatile 變量還能夠提供優於鎖的性能優點。 併發

使用條件框架

您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件: 函數

  • 對變量的寫操做不依賴於當前值。
  • 該變量沒有包含在具備其餘變量的不變式中。

實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。 性能

第一個條件的限制使 volatile 變量不能用做線程安全計數器。雖然增量操做(x++)看上去相似一個單獨操做,實際上它是一個由(讀取-修改-寫入)操做序列組成的組合操做,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操做須要使x 的值在操做期間保持不變,而 volatile 變量沒法實現這點。(然而,若是隻從單個線程寫入,那麼能夠忽略第一個條件。) 測試

反例this

大多數編程情形都會與這兩個條件的其中之一衝突,使得 volatile 變量不能像 synchronized 那樣廣泛適用於實現線程安全。

【反例:volatile變量不能用於約束條件中】 下面是一個非線程安全的數值範圍類。它包含了一個不變式 —— 下界老是小於或等於上界。

public class NumberRange {  
    private volatile int lower;
    private volatile int upper;  
  
    public int getLower() { return lower; }  
    public int getUpper() { return upper; }  
  
    public void setLower(int value) {   
        if (value > upper)   
            throw new IllegalArgumentException(...);  
        lower = value;  
    }  
  
    public void setUpper(int value) {   
        if (value < lower)   
            throw new IllegalArgumentException(...);  
        upper = value;  
    }  
}

lower 和 upper 字段定義爲 volatile 類型不可以充分實現類的線程安全;而仍然須要使用同步——使 setLower()setUpper() 操做原子化。

不然,若是湊巧兩個線程在同一時間使用不一致的值執行 setLowersetUpper 的話,則會使範圍處於不一致的狀態。例如,若是初始狀態是(0, 5),同一時間內,線程 A 調用setLower(4) 而且線程 B 調用setUpper(3),顯然這兩個操做交叉存入的值是不符合條件的,那麼兩個線程都會經過用於保護不變式的檢查,使得最後的範圍值是(4, 3) —— 一個無效值。

volatile的適用場景

模式 #1:狀態標誌

也許實現 volatile 變量的規範使用僅僅是使用一個布爾狀態標誌,用於指示發生了一個重要的一次性事件,例如完成初始化或請求停機。

volatile boolean shutdownRequested;  
  
...  
  
public void shutdown() {   
    shutdownRequested = true;   
}  
  
public void doWork() {   
    while (!shutdownRequested) {   
        // do stuff  
    }  
}

線程1執行doWork()的過程當中,可能有另外的線程2調用了shutdown,因此boolean變量必須是volatile。

而若是使用 synchronized 塊編寫循環要比使用 volatile 狀態標誌編寫麻煩不少。因爲 volatile 簡化了編碼,而且狀態標誌並不依賴於程序內任何其餘狀態,所以此處很是適合使用 volatile。

這種類型的狀態標記的一個公共特性是:一般只有一種狀態轉換shutdownRequested 標誌從false 轉換爲true,而後程序中止。這種模式能夠擴展到來回轉換的狀態標誌,可是隻有在轉換週期不被察覺的狀況下才能擴展(從falsetrue,再轉換到false)。此外,還須要某些原子狀態轉換機制,例如原子變量。

模式 #2:一次性安全發佈(one-time safe publication)

在缺少同步的狀況下,可能會遇到某個對象引用的更新值(由另外一個線程寫入)和該對象狀態的舊值同時存在。

這就是形成著名的雙重檢查鎖定(double-checked-locking)問題的根源,其中對象引用在沒有同步的狀況下進行讀操做,產生的問題是您可能會看到一個更新的引用,可是仍然會經過該引用看到不徹底構造的對象。以下面介紹的單例模式。

private volatile static Singleton instace;     
    
public static Singleton getInstance(){     
    //第一次null檢查       
    if(instance == null){              
        synchronized(Singleton.class) {    //1       
            //第二次null檢查         
            if(instance == null){          //2    
                instance = new Singleton();//3    
            }    
        }             
    }    
    return instance;   
}

模式 #3:獨立觀察(independent observation)

安全使用 volatile 的另外一種簡單模式是:按期 「發佈」 觀察結果供程序內部使用。【例如】假設有一種環境傳感器可以感受環境溫度。一個後臺線程可能會每隔幾秒讀取一次該傳感器,並更新包含當前文檔的 volatile 變量。而後,其餘線程能夠讀取這個變量,從而隨時可以看到最新的溫度值。

使用該模式的另外一種應用程序就是收集程序的統計信息。

【例】以下代碼展現了身份驗證機制如何記憶最近一次登陸的用戶的名字。將反覆使用lastUser 引用來發布值,以供程序的其餘部分使用。(主要利用了volatile的可見性)

public class UserManager {  
    public volatile String lastUser; //發佈的信息  
  
    public boolean authenticate(String user, String password) {  
        boolean valid = passwordIsValid(user, password);  
        if (valid) {  
            User u = new User();  
            activeUsers.add(u);  
            lastUser = user;  
        }  
        return valid;  
    }  
}
模式 #4:「volatile bean」 模式

volatile bean 模式的基本原理是:不少框架爲易變數據的持有者(例如 HttpSession)提供了容器,可是放入這些容器中的對象必須是線程安全的。

在 volatile bean 模式中,JavaBean 的全部數據成員都是 volatile 類型的,而且 getter 和 setter 方法必須很是普通——即不包含約束!

public class Person {  
    private volatile String firstName;  
    private volatile String lastName;  
    private volatile int age;  
  
    public String getFirstName() { return firstName; }  
    public String getLastName() { return lastName; }  
    public int getAge() { return age; }  
  
    public void setFirstName(String firstName) {   
        this.firstName = firstName;  
    }  
  
    public void setLastName(String lastName) {   
        this.lastName = lastName;  
    }  
  
    public void setAge(int age) {   
        this.age = age;  
    }  
}
模式 #5:開銷較低的「讀-寫鎖」策略

若是讀操做遠遠超過寫操做,您能夠結合使用內部鎖volatile 變量來減小公共代碼路徑的開銷。

以下顯示的線程安全的計數器,使用 synchronized 確保增量操做是原子的,並使用 volatile 保證當前結果的可見性。若是更新不頻繁的話,該方法可實現更好的性能,由於讀路徑的開銷僅僅涉及 volatile 讀操做,這一般要優於一個無競爭的鎖獲取的開銷。

public class CheesyCounter {  
    // Employs the cheap read-write lock trick  
    // All mutative operations MUST be done with the 'this' lock held  
    @GuardedBy("this") private volatile int value;  
  
    //讀操做,沒有synchronized,提升性能  
    public int getValue() {   
        return value;   
    }   
  
    //寫操做,必須synchronized。由於x++不是原子操做  
    public synchronized int increment() {  
        return value++;  
    }  
}

使用鎖進行全部變化的操做,使用 volatile 進行只讀操做。
其中,鎖一次只容許一個線程訪問值,volatile 容許多個線程執行讀操做。

單例模式

定義:

確保某個類只有一個實例,並提供一個全局訪問點。

類圖:

public class Singleton{  
    private static final Singleton instance;  
    private Singleton(){  
    }  
    public static Singleton getInstance(){      
        if(instance == null){          //1  
            instance = new Singleton();//2  
        }  
        return instance;               //3  
   }  
    ...  
}

優勢:

  1. 內存中只有一個對象,減小內存開支;
  2. 單例可避免對資源的多重佔用,例如寫文件動做,可避免對同一資源文件的同時寫操做。

缺點:

  1. 單例模式通常沒有接口,擴展很困難; ——單例並非用來繼承的。
  2. 不利於測試,並行開發時,若單例未完成,則不能進行測試;
  3. 與單一職責原則衝突,把「要單例」和業務邏輯融合在一個類中。

使用場景:

若出現多個對象就會出現「不良反應」,應該用單例,具體場景以下:

  1. 要求生成惟一序列號的環境;
  2. 在整個項目中須要一個共享訪問點或共享數據。例如頁面計數器;
  3. 建立一個對象須要消耗的資源過多時;
  4. 須要定義大量的靜態常量和靜態方法的環境。

爲何不直接用全局變量來實現單例?

有缺點:全局變量必須在程序一開始就建立好。而單例模式能夠延遲初始化。

類加載器對單例的影響:

不一樣的類加載器可能會加載同一個類。

若是程序有多個類加載器,可在單例中指定某個加載器,並指定同一個加載器。

多線程的影響:

上文代碼示例在多線程環境下有bug:

  1. 線程 1 調用 getInstance() 方法並決定 instance 在 //1 處爲null
  2. 線程 1 進入 if 代碼塊,但在執行 //2 處的代碼行時被線程 2 預佔。
  3. 線程 2 調用 getInstance() 方法並在 //1 處決定 instancenull
  4. 線程 2 進入 if 代碼塊並建立一個新的 Singleton 對象並在 //2 處將變量instance 分配給這個新對象。
  5. 線程 2 在 //3 處返回 Singleton 對象引用。
  6. 線程 2 被線程 1 預佔。
  7. 線程 1 在它中止的地方啓動,並執行 //2 代碼行,這致使建立另外一個 Singleton 對象。
  8. 線程 1 在 //3 處返回這個對象。
結果是 getInstance() 方法建立了兩個 Singleton 對象。

解決方法一:不用延遲初始化

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

解決方法二:同步getInstance

public class Singleton{  
    private static final Singleton instance;  
    private Singleton(){  
    }  
    //同步getInstance  
    public static synchronized Singleton getInstance(){      
        if(instance == null){          //1  
            instance = new Singleton();//2  
        }  
        return instance;               //3  
   }  
    ...  
}

可是synchronized方法會下降性能,尤爲這裏僅當第一次調用getInstance時才須要同步,只有執行//2代碼行時才須要同步。

你可能想到只同步方法塊,即只對//2進行同步:

public static Singleton getInstance(){      
       if(instance == null){            
           synchronized(Singleton.class) {    
               instance = new Singleton();  
           }  
       }   
       return instance;  
}

但這樣作並不能解決問題:
當 instance 爲 null 時,兩個線程能夠併發地進入if 語句內部。
而後,一個線程進入 synchronized 塊來初始化 instance,而另外一個線程則被阻斷。

    當第一個線程退出 synchronized 塊時,等待着的線程進入並建立另外一個Singleton 對象。

注意:當第二個線程進入 synchronized 塊時,它並無檢查 instance 是否非 null。
仍是會建立2個對象。

解決方法三:雙重檢查加鎖

針對上述方法的缺點,咱們在//2代碼行時 再檢查一次null,就能保證只建立一個對象:

//注意volatile!! 
private volatile static Singleton instace;   
  
public static Singleton getInstance(){   
    //第一次null檢查     
    if(instance == null){            
        synchronized(Singleton.class) {    //1     
            //第二次null檢查       
            if(instance == null){            //2  
                instance = new Singleton();//3  
            }  
        }           
    }  
    return instance;
}

假設有下列事件序列:

  1. 線程 1 進入 getInstance() 方法。
  2. 因爲 instance 爲 null,線程 1 在 //1 處進入synchronized 塊。
  3. 線程 1 被線程 2 預佔。
  4. 線程 2 進入 getInstance() 方法。
  5. 因爲 instance 仍舊爲 null,線程 2 試圖獲取 //1 處的鎖。然而,因爲線程 1 持有該鎖,線程 2 在 //1 處阻塞。
  6. 線程 2 被線程 1 預佔。
  7. 線程 1 執行,因爲在 //2 處實例仍舊爲 null,線程 1 還建立一個Singleton 對象並將其引用賦值給instance(因爲java執行的無序性,可能賦值時只是佔用內存空間(此時instance已經爲非null,鎖鬆開,因爲無序性,尚未來得及初始化,線程2已經取得instance對象),尚未根據構造函數初始化)。
  8. 線程 1 退出 synchronized 塊並從 getInstance() 方法返回實例。
  9. 線程 1 被線程 2 預佔。
  10. 線程 2 獲取 //1 處的鎖並檢查 instance 是否爲 null。
  11. 因爲 instance 是非 null 的,並無建立第二個Singleton 對象,由線程 1 建立的對象被返回,此時返回對象多是是一個構造完整卻沒有徹底初始化的對象。
  12. 線程1繼續執行完成對象的初始化,因爲instance是volatile類型的,因此instance變量對全部線程共享可見,因此線程2能夠獲得一個完整初始化的對象。

對於上面解說的賦值,卻沒有初始化的緣由,是因爲java變量從新賦值時有3個步驟的(讀取,修改,回寫)

代碼行 instance =new Singleton(); 執行了下列僞代碼

1. mem = allocate();             //Allocate memory for Singleton object.
2. instance = mem;               //Note that instance is now non-null, but
                                 //has not been initialized.
3. ctorSingleton(instance);      //Invoke constructor for Singleton passing
                                 //instance.
相關文章
相關標籤/搜索