簡體   English   中英

在java中易失性

[英]Volatile in java

據我所知,在 易失性讀取 之前發生了 易失性寫入 ,所以我們總是會在volatile變量中看到最新的數據。 我的問題基本上涉及到之前發生的術語以及它發生在哪里? 我寫了一段代碼來澄清我的問題。

class Test {
   volatile int a;
   public static void main(String ... args) {
     final Test t = new Test();
     new Thread(new Runnable(){
        @Override
        public void run() {
            Thread.sleep(3000);
            t.a = 10;
        }
     }).start();
     new Thread(new Runnable(){
        @Override
        public void run() {
            System.out.println("Value " + t.a);
        }
     }).start();
   }
}

(為清楚起見,省略了try catch塊)

在這種情況下,我總是看到要在控制台上打印的值0。 沒有Thread.sleep(3000); 我總是看到值10.這是發生在之前關系的情況還是打印'值10',因為線程1開始早一點線程2?

很高興看到每個程序啟動時帶有和不帶volatile變量的代碼行為都不同的例子,因為上面代碼的結果僅取決於(至少在我的情況下)線程順序和線程休眠。

您會看到值0,因為在寫入之前執行了讀取操作。 並且您看到值10,因為寫入在讀取之前執行。

如果您希望測試具有更多不可預測的輸出,則應該讓兩個線程等待CountDownLatch,以使它們同時啟動:

final CountDownLatch latch = new CountDownLatch(1);
new Thread(new Runnable(){
    @Override
    public void run() {
        try {
            latch.await();
            t.a = 10;
        }
        catch (InterruptedException e) {
            // end the thread
        }
    }
 }).start();
 new Thread(new Runnable(){
    @Override
    public void run() {
        try {
            latch.await();
            System.out.println("Value " + t.a);
        }
        catch (InterruptedException e) {
            // end the thread
        }
    }
 }).start();
 Thread.sleep(321); // go
 latch.countDown();

之前發生的事情與寫入在任何后續讀取之前發生有關。 如果寫沒有發生,那么真的沒有關系。 由於寫線程處於休眠狀態,因此在寫入發生之前執行讀取。

要觀察行動中的關系,你可以有兩個變量,一個是volatile,另一個是volatile。 根據JMM,它表示在易失性寫入發生之前,在易失性讀取之前寫入非易失性變量。

例如

volatile int a = 0;
int b = 0;

線程1:

b = 10;
a = 1;

線程2:

while(a != 1);
if(b != 10)
  throw new IllegalStateException();

Java內存模型說b應該總是等於10,因為非易失性存儲發生在volatile存儲之前。 並且在易失性存儲發生之前發生在一個線程中的所有寫入 - 在所有后續的易失性加載之前。

不要堅持“發生在之前”這個詞。 它是事件之間的關系,由jvm在R / W操作調度期間使用。 在這個階段,它不會幫助你了解不穩定性。 重點是:jvm命令所有R / W操作。 jvm可以訂購但它想要(當然服從所有同步,鎖定,等待等)。 現在:如果變量是易失性的,那么任何讀操作都將看到最新寫操作的結果。 如果變量不是volatile,那么就不能保證(在不同的線程中)。 就這樣

我已經重新措辭(粗體字的變化)你問題的第一句中提到的發生前規則,如下所示,以便更好地理解 -

“將易失性變量的值寫入主存儲器 - 在從主存儲器中隨后讀取該變量之前”。

  • 另外需要注意的是, 易失性寫入/讀取總是發生在主存儲器中,而不是發送到/來自寄存器,處理器緩存等任何本地存儲器資源。

上述發生之前的實際含義是, 所有共享volatile變量的線程總是會看到該變量的一致值 在任何給定的時間點,沒有兩個線程看到該變量的不同值。

相反, 共享非易失性變量的所有線程可能在任何給定的時間點看到不同的值,除非它不被任何其他類型的同步機制同步,例如synchronized塊/方法,最終關鍵字等。

現在回到你關於這個發生前的規則的問題,我想你有點誤解了這條規則。 該規則並未規定寫代碼應始終在讀代碼之前發生(執行)。 相反,它決定了如果寫入代碼(揮發性可變寫入)是在一個線程之前在另一個線程然后的寫入代碼的效果應在主存儲器已經發生的讀碼被執行之前所讀取的代碼,使得所執行的讀取代碼可以看到最新的值。

在沒有volatile(或任何其他同步機制)的情況下,這種情況發生 - 之前不是強制性的,因此讀者線程可能會看到非易失性變量的陳舊值,即使它最近由不同的編寫器線程寫入。 因為writer線程可以將值存儲在其本地副本中,並且不需要將值刷新到主內存。

希望以上解釋清楚:)

piotrek是對的,這是測試:

class Test {
   volatile int a = 0;
   public static void main(String ... args) {
     final Test t = new Test();
     new Thread(new Runnable(){
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {}
            t.a = 10;
            System.out.println("now t.a == 10");
        }
     }).start();
     new Thread(new Runnable(){
        @Override
        public void run() {
            while(t.a == 0) {}
            System.out.println("Loop done: " + t.a);
        }
     }).start();
   }
}

揮發性:它將永遠結束

沒有不穩定:永遠不會結束

來自維基:

特別是在Java中,發生在之前的關系是保證由語句A寫入的內存對語句B可見,即語句A在語句B開始讀取之前完成其寫入。

因此,如果線程A用值10寫入ta並且線程B稍后嘗試讀取ta,則發生之前關系保證線程B必須讀取線程A寫入的值10,而不是任何其他值。 這很自然,就像愛麗絲購買牛奶並將它們放入冰箱一樣,然后鮑勃打開冰箱看到牛奶。 但是,當計算機運行時,內存訪問通常不會直接訪問內存,這太慢了。 相反,軟件從寄存器或緩存中獲取數據以節省時間。 它僅在發生緩存未命中時才從內存加載數據。 問題發生了。

讓我們看看問題中的代碼:

class Test {
  volatile int a;
  public static void main(String ... args) {
    final Test t = new Test();
    new Thread(new Runnable(){ //thread A
      @Override
      public void run() {
        Thread.sleep(3000);
        t.a = 10;
      }
    }).start();
    new Thread(new Runnable(){ //thread B
      @Override
      public void run() {
        System.out.println("Value " + t.a);
      }
    }).start();
  }
}

線程A將10寫入值ta,線程B嘗試將其讀出。 假設線程A在線程B讀取之前寫入,然后當線程B讀取它時將從內存加載值,因為它不會將值緩存在寄存器或緩存中,因此它總是由線程A寫入10。如果線程A寫入之后線程B讀取,線程B讀取初始值(0)。 所以這個例子沒有說明易失性是如何起作用的。 但是,如果我們改變這樣的代碼:

class Test {
  volatile int a;
  public static void main(String ... args) {
    final Test t = new Test();
    new Thread(new Runnable(){ //thread A
      @Override
      public void run() {
        Thread.sleep(3000);
        t.a = 10;
      }
    }).start();
    new Thread(new Runnable(){ //thread B
      @Override
      public void run() {
        while (1) {
          System.out.println("Value " + t.a);
        }
      }
    }).start();
  }
}

如果沒有volatile ,打印值應該始終是初始值(0),即使在線程A將10寫入ta之后發生了一些讀取,這違反了之前發生的關系。 原因是編譯器優化代碼並將ta保存到寄存器中,每次使用寄存器值而不是從高速緩沖存儲器讀取時,當然要快得多。 但它也會導致發生之前的關系違規問題,因為線程B在其他人更新之后無法獲得正確的值。

在上面的例子中, 易失性寫入發生 - 在易失性讀取之前意味着在線程A更新之后, 易失性線程B將獲得ta的正確值。 編譯器將保證每次線程B讀取ta時,它必須從緩存或內存中讀取而不是僅使用寄存器的陳舊值。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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