2009-12-14 53 views
6

我想弄清楚下面的代碼是否存在任何潛在的併發問題。具體而言,可見性問題與易變變量有關。 揮發性定義爲:這個變量的值將不會被緩存線程本地:所有讀取和寫入將直接進入「主內存」併發性,對象可見性

public static void main(String [] args) 
{ 
    Test test = new Test(); 

    // This will always single threaded 
    ExecutorService ex = Executors.newSingleThreadExecutor(); 

    for (int i=0; i<10; ++i) 
     ex.execute(test); 
} 

private static class Test implements Runnable { 
    // non volatile variable in question 
    private int state = 0; 

    @Override 
    public void run() { 
     // will we always see updated state value? Will updating state value 
     // guarantee future run's see the value? 
     if (this.state != -1) 
      this.state++; 
    } 
} 

對於上述單線程執行

是否可以做test.state非易失性?換句話說,將會每次連續執行Test.run()(這將會按順序發生而不是同時執行,因爲executor是單線程的),總會看到更新的test.state值?如果沒有,不退出Test.run()確保所做的任何更改使得線程在本地被寫回主內存?否則,當線程在本地進行更改時,如果線程退出時還沒有寫回主內存?

+0

你從哪兒弄來該定義。聽起來像一個1.5 JMM之前的定義(這是不可實現的)。 –

+0

http://www.javamex.com/tutorials/synchronization_volatile.shtml – Integer

+0

認識到線程完成Test.run()時很重要,線程不會終止,並且任何有關由線程在終止之前刷新到主內存不適用。調用Test.run()的'run()'方法線程只是一個循環,它會阻塞,直到它接收到一個要執行的新任務。當該任務從*它的'run()'方法返回時,該線程將阻塞直到下一個任務;它不會終止(從而刷新其狀態)。 – erickson

回答

2

是的,即使執行者在中間替換了它的線程也是安全的。線程啓動/終止也是同步點。

http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.4.4

一個簡單的例子:

static int state; 
static public void main(String... args) { 
    state = 0;     // (1) 
    Thread t = new Thread() { 
     public void run() { 
      state = state + 1; // (2) 
     } 
    }; 
    t.start(); 
    t.join(); 
    System.out.println(state); // (3) 
} 

這是保證(1),(2),(3)良好有序的並且表現爲預期。

對於單線程執行,「任務是保證執行順序」,它必須在開始下一個,這必然正確同步前不知何故檢測一個任務的完成不同run()

-1

如果你的ExecutorService是單線程的,那麼就沒有共享狀態,所以我看不出有什麼問題可以解決。

但是,將Test類的新實例傳遞給​​的每個調用是否更有意義?即

for (int i=0; i<10; ++i) 
    ex.execute(new Test()); 

這樣就不會有任何共享狀態。

+1

這並沒有任何意義,問題的關鍵在於使用同一個對象。 – KernelJ

4

只要它只有一個線程就沒有必要使其變得易變。如果你打算使用多線程,你不僅應該使用volatile,還應該同步。 增加數字不是原子操作 - 這是一個常見的誤解。

public void run() { 
    synchronize (this) { 
     if (this.state != -1) 
      this.state++; 
    } 
} 

而不是使用同步的,你也可以使用AtomicInteger#getAndIncrement()(如果之前如果你不需要的)。

private AtomicInteger state = new AtomicInteger(); 

public void run() { 
    state.getAndIncrement() 
} 
+0

我只是問一個簡單的問題,是否在單線程模式下,其他線程是否會看到狀態變化,並回答了這個問題。然而,越來越多的話題,我不明白爲什麼我需要使用易失性和同步。在我看來,同步就足夠了。 – Integer

+0

我只是想讓你(和其他人在這個問題上磕磕絆絆)意識到使用volatile來遞增一個整數在多線程環境中是不夠的。您將始終需要volatile和synchronized兩者以保證線程安全。 – sfussenegger

+0

易失性通常不足以解決問題的原因很明顯:由於++的非原子性,線程的某些增量可能不會因競爭條件而產生影響。但爲什麼同步沒有波動不足呢?當然,如果你同步讀取。 –

0

您的代碼,具體地該位

  if (this.state != -1) 
        this.state++; 

將需要狀態值的原子測試,然後增量到狀態在併發上下文。所以即使你的變量是不穩定的並且涉及多個線程,你也會遇到併發問題。

但是,您的設計基於聲明總是隻有一個測試實例,,該單個實例僅授予單個(相同)線程。 (但請注意,單實例實際上是主線程和執行程序線程之間的共享狀態。)

我認爲您需要使這些假設更加明確(例如,在代碼中使用ThreadLocal和ThreadLocal 。得到())。這是爲了防範未來的錯誤(當一些其他開發者可能不小心違反了設計假設時),並且防止對關於the Executor method you are using的內部實現的假設,這在某些實現中可能僅僅提供單線程執行程序(即,在execute(runnable)的每個調用中都是順序的而不一定是相同的線程。

+0

聽着,我知道代碼不是一件藝術品。我知道有數百萬個可以改進的地方。是的,我知道threadlocals,我知道關於同步和原子變量。這不是生產代碼。這是代碼來說明我的併發可見性和易變性問題。 – Integer

0

狀態在此特定代碼中是非易失性的,因爲只有一個線程,並且只有該線程才能訪問該字段,這是完全正確的。禁用在你唯一的線程中緩存這個字段的值只會給性能帶來影響。

不過,如果你想使用的狀態的值,在其運行循環的主線程,你必須讓外地揮發性:

for (int i=0; i<10; ++i) { 
      ex.execute(test); 
      System.out.println(test.getState()); 
    } 

然而,即使這樣可能無法正常使用揮發性工作,因爲線程之間沒有同步。

由於該字段是私有的,因此如果主線程執行一個可以訪問該字段的方法,則只有一個問題。

3

原來我是這樣想的:

如果任務總是由 同一個線程中執行,就不會有 問題。但Excecutor產生的 newSingleThreadExecutor()可能會創建 新線程來替換那些因爲任何原因而被殺死的線程。關於什麼時候替換 線程將被創建或哪個線程 將創建它,沒有 保證。

如果一個線程執行某些寫入,然後 上一個新的線程調用start(),這些 寫將是可見的新 線程。但是不能保證該規則適用於這種情況。

但是不可否認的是正確的:創建正確的ExecutorService沒有足夠的障礙來確保可見性實際上是不可能的。我忘記了檢測另一個線程的死亡是同步關係。用於空閒工作線程的阻塞機制也需要一個障礙。

+0

「不能保證何時將創建替換線程或哪個線程創建它。」執行者是否在同一個線程或替換線程中運行Test,它是否有所作爲?因爲我使用的是Test()的同一個實例。我的問題是特定於Test.run()的所有執行是否會看到先前更新的狀態值。 – Integer

+2

是的,不管是否使用同一個線程來運行'Test',都會有所不同。如果一個線程保證運行'run()'的所有執行,那麼這只是普通的單線程編程,並且在該線程的後續調用中肯定會看到更新。但是,因爲任務不使用內存屏障,所以這些更新可能對*不同的線程不可見,包括由執行程序創建的替換線程。 – erickson

+0

謝謝。你完美地回答了我的問題。 – Integer