java中的volatile和synchronized


關於volatile和同步相關的東西,網上有太多錯誤和解釋不清的東西, 所以查閱相關書籍和文章后總結如下, 如果還是也存在不正確的內容,請一定要指出來, 以免誤人子弟:)

1. 原子性與可視性

    原子性是指操作不能被線程調度機制中斷, 除long和double之外的所有基本類型的讀或寫操作都是原子操作,注意這里說的讀寫, 僅指如return i, i = 10, 對於像i++這種操作,包含了讀,加1,寫指令,所以不是原子操作。 對於long和double的讀寫,在64位JVM上會把它們當作兩個32位來操作,所以不具備原子性。

    在定義long和double類型變量時,如果使用volatile來修飾,那么也可以獲得原子性,除此以外,volatile與原子性沒有直接關系。

    可視性,volatile的主要作用就是確保可視性,那么什么是可視性?
    在系統中(多處理器更加明顯),對某一變量的修改有時會暫時保存在本地處理器的緩存中,還沒有寫入共享內存,這時候有另外一個線程讀取變量在共享內存的值,那么這個修改對這個線程就是不可視的。
    Volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。而且,當成員變量發生變化時,強迫線程將變化值直接寫到共享內存。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值。 原子操作不一定就有可視性, 比如賦值,i = 10,  如果i沒有被特別修飾, 那么因為緩存的原因, 它仍然可能是不可視的

    所以原子性和可視性是完全不同的兩個概念

2. volatile的應用場景
    詳細可以參考java語言架構師Brain Geotz的文章
    Java 中volatile 變量可以被看作是一種 “程度較輕的 synchronized”;與 synchronized 塊相比,volatile 優點是所需的編碼較少,並且運行時開銷也較少, 不會引起線程阻塞。Volatile 變量具有 synchronized 的可視性特性,但是不具備原子特性。
    這就導致Volatile 變量可用於提供線程安全,但是應用場景非常有限,在一些經典java書里,基本都不推薦使用volatile替代synchronized來實現同步,因為風險較大, 很容易出錯。
Brain給出的使用volatile實現線程安全的條件:
    對變量的寫操作不依賴於當前值。 (count++這種就不行了)
    該變量沒有包含在具有其他變量的不變式中(Invariants,例如 “start <=end”)。

    我的理解是, 這兩個條件都是因為volatile不能提供原子性導致的, 如果多線程執行的一個操作不是原子性的, 使用volatile時就一定要慎重。
    如果滿足這兩個條件, 多線程執行的操作是原子性的, 那就是可以使用,如:
將 volatile 變量作為狀態標志使用

volatile boolean shutdownRequested;
.
...
public void shutdown() { shutdownRequested = true; } //不依賴當前值,原子操作
public void doWork() { 

  while (!shutdownRequested) { 
  // do stuff
  }
}

 

    文章中的其他幾種模式, 也都差不多這個意思。

    還有一種情況,如果讀操作遠遠超過寫操作,可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。
    結合使用 volatile 和 synchronized 實現 “開銷較低的讀-寫鎖”

@ThreadSafe
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;

  public int getValue() { return value; } //使用volatile替代synchronized

  public synchronized int increment() {

  return value++;

}

 

    然而,你可以在讀操作中使用 volatile 確保當前值的可見性,因此可以使用鎖進行所有變化的操作,使用 volatile 進行只讀操作。其中,鎖一次只允許一個線程訪問值,volatile 允許多個線程執行讀操作,因此當使用 volatile 保證讀代碼路徑時,要比使用鎖執行全部代碼路徑獲得更高的共享度 —— 就像讀-寫操作一樣。

3. synchronized

class AtomTest implements Runnable {
    private volatile int i = 0;
    public int getVal() {return i;}

    public synchronized void inc() {i++; i++;}
    
    @Override
    public void run() {
        while (true) {
            inc();
        }
    }
}

public class TestThread {
    
    public static void main(String[] args) throws InterruptedException {

        ExecutorService exec = Executors.newCachedThreadPool();
        AtomTest at = new AtomTest();
        exec.execute(at);
        while (true) {
            int val = at.getVal();
            if (val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

 

    結果會輸出奇數, 退出程序, 原因是getVal讀到了inc的中間值。 這種情況只能在getVal方法前加synchronized
    在讀取的時候也加鎖, 這樣在讀的時候如果正在寫, 那么等待, 所以就不會讀到inc的中間值。


    關於synchronized值得注意的幾個點:

    1) 所有對象都含有一個鎖,當調用到synchronized(object)塊時,先檢測obj有沒有加鎖,如果有, 阻塞, 如果沒有, 對object加鎖, 執行完后釋放鎖。

    2) synchronized void f() {//...}    等價於  void f() { synchronized(this) {//...} },   在當前對象上加鎖
    3) synchronized 提供原子性和可視性, 被它完全保護的變量不需要用volatile

    4) synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronized f(){},而是變成了f(){}。繼承類需要你顯式的指定它的某個方法為synchronized方法。

 

    注意第一點, 非常重要:雖然sync塊可以包裹一段代碼,但是鎖是加對象上,不是加在代碼上,它的工作機制如下:

    對於synchronized(obj) {//...}: 先檢測obj有沒有加鎖,如果有, 阻塞, 如果沒有, 對obj加鎖, 執行塊中的代碼,完畢后釋放鎖。這里只檢測obj對象上的鎖,不關注代碼塊里的代碼或者對象。

    

    所以, 加鎖的范圍由obj決定,理解了這一點, 下面的很多種情況就會很容易理解:

      1. 當兩個並發線程訪問同一個對象object中的這個相同synchronized(this)同步代碼塊時,一個時間內針對該對象的操作只能有一個線程得到執行。另一個線程必須等待。

         - 如果同一對象已經加鎖, 另一線程執行到sync塊,檢測到有鎖掛起。

  2. 然而,另一個線程仍然可以訪問該object中的synchronized(this)同步代碼塊。

         - 非sync代碼,不關注對象是否加鎖

  3. 當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對該object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。

         - synchronized(this) 只是關注this對象, 只要this已加鎖,執行到同一對象中不同方法的sync塊時,也會阻塞。

  4. 不同的對象實例的synchronized(this)方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。

         - 可以同時訪問不同對象中的sync塊, 原因很簡單, 因為synchronized(this),關注的是this對象,不同對象的this是不一樣的。

      5.  同理,也可以對其他對象加鎖,

          1) 對於類中的成員對象: private Integer i = new Integer(0);   synchronized(i) {//...}    i創建在堆上,每個對象有一個i,所以效果與synchronized(this)一樣。

          2) 對於靜態成員對象: private static Integer i = new Integer(0);  synchronized(i) {//...}: i創建在靜態區,屬於類, 所以效果與synchronized(ClassName.class)一樣。

  6.對類對象加鎖時,對該類的所有對象都起作用:synchronized(ClassName.class) {//...}

 

最后我在測試代碼時,發現對於private Integer i = 0;   synchronized(i) {//...}

鎖的效果是全局的,推測可能是Integer對0進程打包時,自動生成的這個對象可能在常量區。 后來查了下資料才發現並非如此:

Integer實現中有一個IntegerCache類,它包含一個靜態的Integer數組,在類加載時就將-128 到 127 的Integer對象創建了,並保存在cache數組中,一旦程序調用valueOf 方法,如果i的值是在-128 到 127 之間就直接在cache緩存數組中去取Integer對象。 所以這里的i引用的是整個全局數組里值, 所以鎖也是全局的了。。。

如果改成private Integer i = 300,  然后加鎖就只在本對象有效了。 原因是i不在緩存范圍,所以創建在了堆上。

refer  http://blog.csdn.net/xiaohai0504/article/details/6885137

 


免責聲明!

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



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