2016-01-12 20 views
1

我需要一些幫助,以充分了解發生了什麼時,運行此代碼併發修飾的變量:不能完全理解這個例子

public class Main extends Thread { 

    private static int x; 

    public static void main(String[] args) { 
     Thread th1 = new Main("A"); 
     Thread th2 = new Main("B"); 
     th1.start(); 
     th2.start(); 
    } 

    public Main(String n) { 
     super(n); 
    } 

    public void run() { 
     while(x<4) { //1 
      x++;  //2 
      System.out.print(Thread.currentThread().getName()+x+" "); //3 
     } 
    } 
} 

我得到的輸出

B2 B3 B4 A2 

我明白線程AB都增加x,然後B循環遞增並輸出...但爲什麼最後輸出A2?當執行//3時不應該A看到x爲4?

紅利問題:爲什麼x不可能變成5?

編輯

這個問題(在一個稍微不同的形式)來自一個模擬測試OCP認證,其中解釋說x永遠5.我很高興地看到,我不是唯一一個不同意。

+0

使'x'變得易變。這是一個基本的內存可見性問題。 –

+0

我不想更改代碼,我只需要了解它的工作原理。這只是一個例子 –

+1

然後,也許你應該試着理解爲什麼添加'volatile'會使它工作如何你期望的。 http://stackoverflow.com/questions/4885570/what-does-volatile-mean-in-java –

回答

5

當您在一個線程中更新變量的值時,其值不一定對所有線程立即可見。這是因爲內存保存在CPU高速緩存中,這使得它可以比主內存更快地讀取和寫入。

定期將更新後的緩存內容複製到主內存中。只有當這種情況發生時,其他線程纔會看到更新值。

它看起來像在這裏發生的是B正在更新值,但該值沒有提交給主內存;因此,A看到了它的舊價值。

如果您創建變量volatile,則所有讀寫都直接從/到主內存完成(或者,至少將緩存從/刷新到主內存),因此對值的更新立即可見所有線程。

但是,請注意,您未執行原子讀取和寫入操作:另一個線程可能會在當前線程檢查x < 4和增量x++之間更新值x。因此,您可能最終打印的值爲5

解決這個問題的最簡單的方法是使檢查/增量同步:

synchronized (Main.class) { 
    if (x < 4) { 
    x++; 
    System.out.println(...); 
    } 
} 

這也有保證更新的可視性x中的所有線程的效果,又能保證只有一個線程可以一次檢查/增加x

+0

從(另一個)Andy閱讀答案,我只是把'x''靜態變量'變成了'A2 A3 A4 B2'。看起來像'volatile'並不是這裏的關鍵......我錯了嗎? –

3

這是一個典型的競賽條件。當你呼叫th1.start() & th2.start()它只有時間表線程開始,它不會順序開始。因此,您的實際線程可以並且以任何舊的順序開始。現在,再加上while (x<4)x++System.out.println之間的事實,任何一個線程都可以調度並允許另一個線程運行,並且基本上出現未定義的行爲。

紅利問題:爲什麼x不可能變成5?

這不是不可能的(出於同樣的原因,輸出是交錯的)。嘗試增加你的線程數量,最終你會看到x變爲5,甚至更高,這取決於你可以創建多少線程爭用。

我不同意他人這是一個波動性問題。相反,這是一個共享內存訪問問題。單獨使用volatile無法解決此問題。圍繞靜態變量訪問的一個簡單的互斥鎖將適當地保護它並按照您期望的順序排列,除了需要額外同步的'A'和'B'的順序之外。

+0

你的意思是說,單獨使用'volatile'就不能保證在每個'print()'處x值都會增加一個嗎? –

+0

是的。單獨使用'volatile'不能保證。即使使用'volatile',您仍然可以輕鬆擁有'B1 B3 A4'等。 – Andy

+0

當然!你是對的,我剛剛意識到這一點。 –

0

您的變量xstatic因此它在兩個線程之間共享。

線程B遞增x4並完成,寫下每一步。

主題A有一次機會看x當它在1因此遞增並打印A2。下一次它看到x它在>= 4因此它退出它的循環。

獎金問題 - 是的,它是可能x成爲5 - 甚至打印爲5。如果線程檢查x<4當它碰巧是3在同一時間,他們都會增加它。

0

知道啓動是異步方法調用,所以第一個關閉的線程將在另一個之前啓動。 二:x是一個靜態的,但在本地的上下文意味着第一個正在運行的線程將改變x,而第二個仍然在睡覺(當第二個睡眠時有一個本地存儲的本地靜態x值,他會在喚醒後使用) 之後,一旦第二個線程打印本地x,他將在內存(全局一)上尋找它,並找到它等於4,所以他會停下來。

這可能有助於

| -------------------------------------- -------------------------------------------------- - |

|主題A:x works | local |大靜態X改變了。 。 。 。 。 。 。 。 。 。 。 。 。 。 .. |

|線程B:x = 2 sleep | local |在第一次循環之後會被讀取的大靜態X. |

| --------------------------------------------- --------------------------------------------- |

所以我們可以說X是局部和全局的同時

證明:增加睡眠與隨機的時間,看看X < 10後的結果增量不要忘了嘗試catch子句。

2

你,我的朋友,遇到了所謂的Data Race

維基百科有一個例子描述了你正在經歷的事情: https://en.wikipedia.org/wiki/Race_condition

那麼,爲什麼會發生這種情況呢? 原因隱藏在計算機處理指令的方式中。舉個例子來說,Java代碼的下面一行:現在

x++; 

,無視魔編譯的時刻,我們不得不思考一下電腦需要做的,執行這一指令。

  1. 我們需要讀取x的舊值。
  2. 我們需要執行加法x + 1.
  3. 我們需要將新值寫回變量x。

從順序的角度來看,這很有用。但是如果兩個人同時做同樣的事情會發生什麼呢?

請參閱維基百科示例以獲取確切答案。

這裏需要注意的重要一點是,您的單條指令實際上是針對計算機的多條指令。即使每條指令都可以由處理器以原子方式執行,但您不能保證整個指令序列的原子性。

這同樣適用於使用變量x。當您調用System.out.println()函數時,您將再次訪問x。這個訪問意味着我們必須再次從內存中讀取x。

我們知道B在變更變量時對變量做了什麼嗎? 沒有。

此外,我注意到volatile評論。這實際上是錯誤的(通過在我的計算機上運行代碼確認)。 volatile確保我們不會將混亂的數據讀取/寫入變量。它不能確保任何其他原子性。

紅利問題:爲什麼x不可能變成5?

這是非常可能的,雖然也許不太可能。需要時間的程序部分是在System.out.println()聲明中完成的工作和同步。這可能是你經常看不到價值5的原因。

+0

感謝您成爲朋友! =)無論如何,在這一點上,我真的很困惑什麼'揮發性'真的... –

+0

@LuigiCortese對不起,並不意味着如此粗暴。事實上,易變的關鍵字,這是相當模糊的。一般來說,你永遠不能保證在閱讀變量時你甚至可以閱讀合理的數據。你可能會讀/寫一些可怕的錯誤的更新值(比如當前示例中的100483)。 volatile關鍵字將確保不會發生,並且您始終讀取該值而不是將其存儲在寄存器中。 然而,現在大多數PC計算機都運行一個內存模型,它實際上保證了這種讀/寫不會發生。我不知道是否同樣適用於服務器。 –

+0

你一直都不是「粗暴」!新單詞添加到我的英語詞彙! –