2017-02-23 77 views
2

我正在學習volatile變量。我知道什麼是volatile,我爲volatile變量編寫了一個示例程序,但沒有按預期工作。Java volatile關鍵字按預期工作

爲什麼「計數」未來一段時間小於2000年的終值我已經使用揮發性因此係統不應該緩存「計數」變量和值應始終是2000

當我用同步方法工作正常,但不是在volatile關鍵字的情況下。

public class Worker { 

private volatile int count = 0; 
private int limit = 10000; 

public static void main(String[] args) { 
    Worker worker = new Worker(); 
    worker.doWork(); 
} 

public void doWork() { 
    Thread thread1 = new Thread(new Runnable() { 
     public void run() { 
      for (int i = 0; i < limit; i++) { 

        count++; 

      } 
     } 
    }); 
    thread1.start(); 
    Thread thread2 = new Thread(new Runnable() { 
     public void run() { 
      for (int i = 0; i < limit; i++) { 

        count++; 

      } 
     } 
    }); 
    thread2.start(); 

    try { 
     thread1.join(); 
     thread2.join(); 
    } catch (InterruptedException ignored) {} 
    System.out.println("Count is: " + count); 
} 
} 

謝謝您提前!

+1

易失性和同步是不一樣的...使用原子代替... –

+2

*「我知道什麼易變」*沒有冒犯,但這個問題證明,否則。 – Tom

+1

「我使用了volatile,因此係統不應該緩存」count「variable」,除了競爭條件(這裏顯而易見的問題),這是完全錯誤的。易失性不會阻止CPU緩存內存中的值 - 甚至沒有辦法告訴現代CPU做這樣的事情,因爲它毫無意義。 Volatile做了一些非常不同的事情,如果不瞭解內存排序和可視性保證是什麼(並且在瞭解它之後你會注意到有更好的高級解決方案)。 – Voo

回答

8

當你做count++,這是一個讀,增量,然後寫。兩個線程可以分別執行讀取操作,每個線程執行增量操作,然後每個線程執行寫入操作,結果只產生一次增量。雖然您的讀取是原子性的,但您的寫入是原子性的,沒有值被緩存,這是不夠的。您需要的不僅僅是這些 - 您需要一個原子讀取 - 修改 - 寫入操作,並且volatile不提供此操作。

+0

很好的解釋! –

2

count++基本上是這樣的:

// 1. read/load count 
// 2. increment count 
// 3. store count 
count = count + 1; 

單獨的firstthird操作是原子的。他們所有3個together都不是原子的。

1

i++ is not atomic in Java。因此兩個線程可以同時讀取,兩者都計算爲+1爲相同的數字,並且兩者都存儲相同的結果。

編譯這個使用javac inc.java

public class inc { 
    static int i = 0; 
    public static void main(String[] args) { 
     i++; 
    } 
} 

閱讀使用javap -c inc字節碼。我修剪下來,只是顯示功能main()

public class inc { 
    static int i; 

    public static void main(java.lang.String[]); 
    Code: 
     0: getstatic  #2     // Field i:I 
     3: iconst_1 
     4: iadd 
     5: putstatic  #2     // Field i:I 
     8: return 
} 

我們看到,增量(靜態INT)的使用來實現:getstaticiconst_1iaddputstatic

由於這是用四條指令完成的,並且沒有鎖,所以不會有原子性的期望。另外值得一提的是,即使這是用1條指令完成,我們可能是出於運氣(報價從this thread用戶「熱舔」的評論):

即使在硬件實現的「增量存儲位置」指令,但不能保證這是線程安全的。僅僅因爲一個操作可以表示爲一個單一的操作員,並沒有說它是線程安全的。


如果你真的想解決這個問題,你可以使用AtomicInteger,具有原子性的保證:

final AtomicInteger myCoolInt = new AtomicInteger(0); 
myCoolInt.incrementAndGet(1); 
1

當你使用​​方法,它正在爲預期的,因爲它確保瞭如果其中一個線程執行該方法,其他調用者線程的執行將暫停,直到當前正在執行的線程退出該方法。在這種情況下,整個讀 - 增量 - 寫週期是原子的。

tutorial

首先,它是不可能的同一對象來交織上同步方法 兩個調用。當一個線程正在爲一個對象執行一個同步方法時,所有其他線程將爲同一對象塊(暫停執行) 調用 同步方法,直到第一個線程完成對象。其次,當一個同步方法退出時,它會自動建立一個 事件之前的關係,以及任何後續調用同一對象的同步方法。這保證了對對象狀態的更改 對所有線程均可見。

當您使用volatile(因爲它是由其他人解釋)這個週期是不是原子作爲使用該關鍵字並不能保證會有對get和增量步驟之間的其他線程的變量沒有其他的寫在這個線程上。

對於原子計數而不是​​關鍵字,您可以使用例如一個AtomicInteger

public class Worker { 
    private AtomicInteger count = new AtomicInteger(0); 
    private int limit = 10000; 

    public static void main(String[] args) { 
     Worker worker = new Worker(); 
     worker.doWork(); 
    } 

    public void doWork() { 
     Thread thread1 = new Thread(new Runnable() { 
      public void run() { 
       for (int i = 0; i < limit; i++) 
        count.getAndIncrement(); 
      } 
     }); 
     thread1.start(); 
     Thread thread2 = new Thread(new Runnable() { 
      public void run() { 
       for (int i = 0; i < limit; i++) 
        count.getAndIncrement(); 
      } 
     }); 

     thread2.start(); 

     try { 
      thread1.join(); 
      thread2.join(); 
     } catch (InterruptedException ignored) { 
     } 
     System.out.println("Count is: " + count); 
    } 
} 

這裏getAndIncrement()確保原子讀增量集週期。

1

內存可見性和原子是多線程中兩個不同但常見的問題。當您使用同步關鍵字時,它通過獲取鎖確保兩者。而volatile只解決內存可見性問題。 Brain Goetz在他的書Concurrency in practice中解釋了何時應該使用volatile。

  1. 寫入變量不取決於其當前值,或者您可以確保只有單個線程更新該值;
  2. 該變量不參與不變量與其他狀態 變量;
  3. 由於任何其他原因,在訪問變量 時不需要鎖定。

那麼,在你的情況看看操作計數++不是原子的。