java中的volatile和synchronized

關於volatile和同步相關的東西,網上有太多錯誤和解釋不清的東西, 因此查閱相關書籍和文章後總結以下, 若是仍是也存在不正確的內容,請必定要指出來, 以避免誤人子弟:)html

1. 原子性與可視性java

    原子性是指操做不能被線程調度機制中斷, 除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
  }
}

 

    文章中的其餘幾種模式, 也都差很少這個意思。ide

    還有一種狀況,若是讀操做遠遠超過寫操做,能夠結合使用內部鎖和 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

相關文章
相關標籤/搜索