簡體   English   中英

為什么標記Java變量volatile會降低同步性?

[英]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中的volatilesynchronized關鍵字之間存在細微差別。 例如, 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM