[英]Why does marking a Java variable volatile make things less synchronized?
所以我剛剛學習了volatile關鍵字,同時為明天的TAing部分編寫了一些示例。 我寫了一個快速程序來證明++和 - 操作不是原子的。
public class Q3 {
private static int count = 0;
private static class Worker1 implements Runnable{
public void run(){
for(int i = 0; i < 10000; i++)
count++; //Inner class maintains an implicit reference to its parent
}
}
private static class Worker2 implements Runnable{
public void run(){
for(int i = 0; i < 10000; i++)
count--; //Inner class maintains an implicit reference to its parent
}
}
public static void main(String[] args) throws InterruptedException {
while(true){
Thread T1 = new Thread(new Worker1());
Thread T2 = new Thread(new Worker2());
T1.start();
T2.start();
T1.join();
T2.join();
System.out.println(count);
count = 0;
Thread.sleep(500);
}
}
}
正如預期的那樣,該計划的產出大致如下:
-1521
-39
0
0
0
0
0
0
但是,當我改變時:
private static int count = 0;
至
private static volatile int count = 0;
我的輸出更改為:
0
3077
1
-3365
-1
-2
2144
3
0
-1
1
-2
6
1
1
我讀過你什么時候在Java中使用volatile關鍵字? 所以我覺得我已經對關鍵字的作用有了基本的了解(在不同的線程中保持變量的緩存副本的同步,但不是read-update-write safe)。 我知道這段代碼當然不是線程安全的。 對我的學生來說,作為一個例子,特別不是線程安全的。 但是,我很好奇為什么添加volatile關鍵字使得輸出不像關鍵字不存在時那樣“穩定”。
為什么標記Java變量volatile會降低同步性?
使用volatile
關鍵字“為什么代碼運行更糟”的問題不是一個有效的問題。 由於用於易失性字段的不同內存模型,它的行為不同 。 您的程序輸出在沒有關鍵字的情況下趨於0的事實無法依賴,如果您轉移到具有不同CPU線程或CPU數量的不同體系結構,則不同的結果並不罕見。
此外,重要的是要記住雖然x++
似乎是原子的,但它實際上是一個讀/修改/寫操作。 如果在許多不同的體系結構上運行測試程序,您會發現不同的結果,因為JVM實現volatile
方式與硬件有關。 訪問volatile
字段也可能比訪問緩存字段慢得多 - 有時會增加1或2個數量級,這將改變程序的時間。
使用volatile
關鍵字會為特定字段建立內存屏障,並且(從Java 5開始)此內存屏障將擴展到所有其他共享變量。 這意味着在訪問時,變量的值將被復制到中央存儲中。 但是,Java中的volatile
和synchronized
關鍵字之間存在細微差別。 例如, volatile
不會發生鎖定,因此如果多個線程正在更新volatile變量,則非原子操作周圍將存在競爭條件。 這就是為什么我們使用AtomicInteger
和朋友,它們在沒有同步的情況下適當地處理增量函數。
這里有一些關於這個主題的好讀物:
希望這可以幫助。
對你所看到的內容進行了有根據的猜測 - 當沒有標記為volatile時,JIT編譯器正在使用x86 inc / dec操作,它可以自動更新變量。 一旦標記為volatile,就不再使用這些操作,而是讀取變量,遞增/遞減,然后最終寫入導致更多“錯誤”。
非易失性設置無法保證它能夠很好地運行 - 在不同的架構上,它可能比標記為volatile時更糟。 將該區域標記為易失性並未開始解決此處出現的任何種族問題。
一種解決方案是使用AtomicInteger類,它允許原子增量/減量。
易失性變量就像每個交互都包含在同步塊中一樣。 正如您所提到的,遞增和遞減不是原子的,這意味着每個遞增和遞減包含兩個同步區域(讀取和寫入)。 我懷疑增加這些偽碼會增加操作沖突的可能性。
通常,兩個線程將具有與另一個線程的隨機偏移,這意味着任何一個線程覆蓋另一個線程的可能性是均勻的。 但是由volatile引起的同步可能迫使它們處於反向鎖步狀態,如果它們以錯誤的方式嚙合在一起,則會增加錯過增量或減量的機會。 此外,一旦他們進入這個鎖步,同步使他們不太可能突破它,增加偏差。
我偶然發現了這個問題,在玩了一段代碼后發現了一個非常簡單的答案。
在初始預熱和優化(零前的前兩個數字)之后,當JVM全速工作時, T1
只是在 T2
開始之前啟動並完成 ,因此count
一直上升到10000然后再到0.當我將工作線程中的迭代次數從10000更改為100000000,輸出非常不穩定,每次都不同。
添加volatile
時輸出不穩定的原因是它使代碼慢得多,即使有10000次迭代, T2
也有足夠的時間啟動並干擾T1
。
所有這些零的原因並不是 ++和 - 是相互平衡的。 其原因是,這里沒有什么導致count
的循環線程影響count
在主線程。 您需要同步塊或易失性count
(“內存屏障”)來強制JVM使所有內容看到相同的值。對於您的特定JVM /硬件,最有可能發生的是,值始終保存在寄存器中,從來沒有得到緩存 - 更不用說主存了 - 根本沒有。
在第二種情況下,你正在按照你的意圖行事:同一course
非原子增量和減量,並得到與預期相似的結果。
這是一個古老的問題,但需要說明每個線程保留它自己的獨立數據副本。
如果您看到count
的值不是 10000的倍數,則只會顯示您的優化程序較差。
它不會“減少同步”。 它使它們更加同步,因為線程總是“看到”變量的最新值。 這需要建立具有時間成本的記憶障礙。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.