2012-01-22 23 views
9

「Java Concurrency in Practice」給出了以下不安全類的示例,這些類由於java內存模型的性質可能會永久運行或打印爲0.Java內存模型同步:如何誘發數據可見性的bug?

此類嘗試演示的問題是變量線程之間不會「共享」。因此,線程看到的值可能與另一個線程不同,因爲它們不是易失性的或同步的。另外,由於JVM ready = true允許的語句的重新排序可能會在number = 42之前設置。

對於我來說,這個類總是使用JVM 1.6工作正常。關於如何讓這個類執行不正確的行爲(即打印0或永遠運行)的任何想法?

public class NoVisibility { 
    private static boolean ready; 
    private static int number; 

    private static class ReaderThread extends Thread { 
     public void run() { 
      while (!ready) 
       Thread.yield(); 
      System.out.println(number); 
     } 
    } 

    public static void main(String[] args) { 
     new ReaderThread().start(); 
     number = 42; 
     ready = true; 
    } 
} 
+4

簡短的回答沒有。但是你真的需要檢查在有人告訴你之後從屋頂跳下是否危險,並給出了充分的理由嗎? – Voo

+1

使用2芯電腦。 –

+1

@Voo ...你到底在說什麼? –

回答

6

您遇到的問題是,您沒有足夠長的時間來優化代碼並緩存值。

當x86_64系統上的線程第一次讀取值時,它將獲得線程安全副本。它只能在以後的變化中看不到。其他CPU可能不是這種情況。

如果您嘗試此操作,則可以看到每個線程都卡住了其本地值。

public class RequiresVolatileMain { 
    static volatile boolean value; 

    public static void main(String... args) { 
     new Thread(new MyRunnable(true), "Sets true").start(); 
     new Thread(new MyRunnable(false), "Sets false").start(); 
    } 

    private static class MyRunnable implements Runnable { 
     private final boolean target; 

     private MyRunnable(boolean target) { 
      this.target = target; 
     } 

     @Override 
     public void run() { 
      int count = 0; 
      boolean logged = false; 
      while (true) { 
       if (value != target) { 
        value = target; 
        count = 0; 
        if (!logged) 
         System.out.println(Thread.currentThread().getName() + ": reset value=" + value); 
       } else if (++count % 1000000000 == 0) { 
        System.out.println(Thread.currentThread().getName() + ": value=" + value + " target=" + target); 
        logged = true; 
       } 
      } 
     } 
    } 
} 

打印下面顯示其fliling值,但卡住了。

Sets true: reset value=true 
Sets false: reset value=false 
... 
Sets true: reset value=true 
Sets false: reset value=false 
Sets true: value=false target=true 
Sets false: value=true target=false 
.... 
Sets true: value=false target=true 
Sets false: value=true target=false 

如果我添加-XX:+PrintCompilation此開關發生了時間,你看到

1705 1 % RequiresVolatileMain$MyRunnable::run @ -2 (129 bytes) made not entrant 
1705 2 % RequiresVolatileMain$MyRunnable::run @ 4 (129 bytes) 

這表明代碼已經被編譯爲本地是不是線程安全的方式。

,如果你讓volatile你看到它不斷地翻轉值的值(或直到我厭倦)

編輯:這是什麼做的測試是;當它檢測到的值不是那個線程目標值時,它設置該值。即。線程0設置爲true,線程1設置爲false當兩個線程正確共享字段時,他們會看到對方發生更改,並且值不斷在true和false之間翻轉。

沒有volatile會失敗,每個線程只能看到它自己的值,所以它們都改變了值,線程0看到true,線程1看到false爲同一個字段。

+1

您能否提供一個可以證明問題的循環示例? –

+0

不知道我明白了! –

+0

我已經添加了解釋。讓我知道你是否有更多的疑問/問題。 –

1

根據您的操作系統,Thread.yield()可能會也可能不會。 Thread.yield()不能真正被認爲是平臺獨立的,如果你需要這個假設,就不應該使用Thread.yield()。

讓示例做你期望的事情,我認爲這更多的是處理器體系結構,而不是其他任何事情......嘗試在不同的機器上運行它,使用不同的操作系統,看看你能從中得到什麼。

+0

這與thread.yield有關......它關於java中的共享內存模型。 –

2

不是100%肯定這一點,但this可能涉及:

什麼是重新排序意味着什麼?

有許多箱子在其訪問可 出現在一個不同的順序是由 程序指定要執行到程序變量 (對象的實例字段,類靜態字段,和數組元素)。編譯器可以自由地以最優化的名義排序 指令。在某些情況下,處理器可能會執行 指令。數據可能是 在 寄存器,處理器高速緩存和主存儲器之間移動的順序與程序指定的順序不同。

例如,如果一個線程寫入字段,然後到現場b和 b的值不依賴於a的值,那麼編譯器是 自由地重新排列這些操作,並且緩存免費刷新b到 之前的主內存。有許多潛在的重新排序源,例如編譯器,JIT和緩存。

編譯器,運行時和硬件都應該合謀創建 作爲-如果串行語義,這意味着在一個 單線程程序,該程序不應該能夠觀察 效果的幻覺的重新訂購。但是,重排序可能會在 錯誤同步的多線程程序中發揮作用,其中一個線程是 能夠觀察其他線程的影響,並且可能能夠 檢測到變量訪問變得對其他線程可見 不同於在程序中執行或指定。

+0

+1打印輸出0是由於重新排序。從這本書本身來看:「NoVisibility可能會打印爲零,因爲寫入就緒可能會在寫入數字之前對讀者線程可見,這種現象稱爲重新排序」 – zengr

6

java內存模型定義了什麼是工作所需要的,什麼不是。不安全的多線程代碼的「美」是在大多數情況下(尤其是在控制開發環境中)它通常起作用。只有當你用更好的計算機進行生產並且負載增加時,JIT纔會真正開始發現bug。

+0

「並非我相信的答案。 – zengr

+1

不是一個肯定的答案,而是一個粗糙的現實! +1 – mawia

2

我認爲關於這一點的要點是不能保證所有的jvms都會以相同的方式重新排序指令。它用作存在不同可能重排序的示例,因此對於jvm的某些實現,您可能會得到不同的結果。恰巧你每次都以同樣的方式對jvm進行重新排序,但是對於另一個可能不是這樣。保證排序的唯一方法是使用適當的同步。

0

請參閱下面的代碼,它介紹了x86上的數據可見性錯誤。 試圖與jdk8和JDK7

package com.snippets; 


public class SharedVariable { 

    private static int sharedVariable = 0;// declare as volatile to make it work 
    public static void main(String[] args) throws InterruptedException { 

     new Thread(new Runnable() { 

      @Override 
      public void run() { 
       try { 
        Thread.sleep(1000); 
       } catch (InterruptedException e) { 
        e.printStackTrace(); 
       } 
       sharedVariable = 1; 
      } 
     }).start(); 

     for(int i=0;i<1000;i++) { 
      for(;;) { 
       if(sharedVariable == 1) { 
        break; 
       } 
      } 
     } 
     System.out.println("Value of SharedVariable : " + sharedVariable); 
    } 

} 

訣竅是不要指望處理器做了重新排序,而讓 編譯器做了一些優化,其引入了可視性缺陷。

如果你運行上面的代碼,你會看到它無限期地掛起,因爲它從來沒有看到更新的值sharedVariable。

要更正代碼,將sharedVariable聲明爲volatile。

爲什麼正常變量不起作用,上述程序掛起?

  1. sharedVariable未被聲明爲volatile。
  2. 現在因爲sharedVariable沒有被聲明爲易失性編譯器優化了代碼。 它看到sharedVariable不會改變,所以爲什麼我應該在循環中每次從內存中讀取 。它會將共享變量從循環中取出。類似於下面的東西。

˚F

for(int i=0;i<1000;i++)/**compiler reorders sharedVariable 
as it is not declared as volatile 
and takes out the if condition out of the loop 
which is valid as compiler figures out that it not gonna 
change sharedVariable is not going change **/ 
    if(sharedVariable != 1) { 
    for(;;) {} 
    }  
} 

在github上共享:https://github.com/lazysun/concurrency/blob/master/Concurrency/src/com/snippets/SharedVariable.java