2011-06-12 392 views
3

我想了解一下我最近的多線程思想。這裏有:避免使用狀態變量副本進行阻塞?

假設我有以下(僞)類,它的run()方法在某個線程上永遠徘徊。其他線程將在隨機時間使用setState()更改Foo實例的狀態。 run()所做的工作只涉及讀取狀態變量,不寫入,並且在while語句的一次執行過程中(例如:繪製位圖),狀態不得更改。在這種情況下,擁有2個狀態變量副本似乎可以防止很多潛在的阻塞(因爲如果我只有一個共享狀態變量副本,我將不得不同步while循環中的所有內容(使用stateLock)而外部線程可能沒有機會改變狀態)。代碼中斷後的問題。

class Foo { 
    Object stateLock = new Object(); 

    private float my1, my2, my3; 
    private float sh1, sh2, sh3; // sh stands for shared 

    public void setState(...) { 
    synchronized (stateLock) { 
     // modify sh1, sh2, or sh3 here 
    } 
    } 

    private void updateState() { 
    synchronized (stateLock) { 
     // set my1=sh1, my2=sh2, my3=sh3 
    } 
    } 

    public void run() { 
    while(true) { 
     updateState(); 
     // then do tons of stuff that uses my1,my2,my3 over and over... 
     ... 
    } 
    } 
} 

這個邏輯的任何漏洞?有沒有一種「標準化」或更聰明的方式來做到這一點?如果有大量的狀態變量呢?更糟糕的是,如果狀態變量是不容易複製的自定義對象(例如,在Java中,自定義對象的變量是引用)?

順便說一下,這來自我目前在Android中使用SurfaceView的工作。

回答

2

要使所有變量保持同步並避免同步,可以將變量放入不可變對象中並作爲一個整體進行更新。讀取狀態時,請將一個狀態對象保持爲局部變量,並保證在您讀取它時沒有其他人更新它。

這是一些示例代碼(未經測試等)。如果舊值不能在setState中讀取,或者只能從一個線程訪問,那麼一個易失性字段就足夠了。但在一般情況下(多線程調用setState並且新狀態取決於舊狀態的值),使用AtomicReference可以確保不會更新。

class Foo { 
    private final AtomicReference<State> state = new AtomicReference<State>(new State(0, 0, 0)); 

    private void setState(float x1, float x2, float x3) { 
     State current; 
     State updated; 
     do { 
      current = state.get(); 
      // modify the values 
      float sh1 = current.sh1 + x1; 
      float sh2 = current.sh2 + x2; 
      float sh3 = current.sh3 + x3; 
      updated = new State(sh1, sh2, sh3); 
     } while (!state.compareAndSet(current, updated)); 
    } 

    public void run() { 
     while (true) { 
      State snapshot = state.get(); 
      // then do tons of stuff that uses sh1, sh2, sh3 over and over... 
     } 
    } 

    private class State { 
     public final float sh1, sh2, sh3; 

     State(float sh1, float sh2, float sh3) { 
      this.sh1 = sh1; 
      this.sh2 = sh2; 
      this.sh3 = sh3; 
     } 
    } 
} 

下面是特殊情況下的示例代碼,更新狀態不依賴於國家的舊值:

class Foo { 
    private volatile State state = new State(0, 0, 0); 

    private void setState(float sh1, float sh2, float sh3) { 
     state = new State(sh1, sh2, sh3); 
    } 

    public void run() { 
     while (true) { 
      State snapshot = state; 
      // then do tons of stuff that uses sh1, sh2, sh3 over and over... 
     } 
    } 

    private class State { 
     public final float sh1, sh2, sh3; 

     State(float sh1, float sh2, float sh3) { 
      this.sh1 = sh1; 
      this.sh2 = sh2; 
      this.sh3 = sh3; 
     } 
    } 
} 
+0

這是打包這個過程的好方法(並且沒有任何明確的同步---獎金!)。謝謝! (我碰巧在你的文章後面閱讀了Java併發實踐中的這一章。) – heycosmo 2011-06-13 07:05:18

+0

我還沒有閱讀「實踐中的Java併發性」,但可能我從其他幾百個地方收集了相同的信息。 :) – 2011-06-14 22:29:52

3

有修復此的簡單的方法是:

private volatile float sh1, sh2, sh3; // note "volatile" 

在Java內存模型,線程被允許從其他線程的緩存值。 volatile關鍵字表示所有線程都必須使用相同的變量值(即全部引用變量的相同內存位置)。當與基元一起使用時,這意味着您不需要同步(儘管使用64位基元,其中浮點數爲而非之一,但您可能不需要同步,這取決於您的JVM是32位還是64位)

您可能想要觀察部分/不一致的更新 - 其中sh變量的一些在另一個線程讀取它們時被更新。您可能希望通過更新多個sh變量「原子」來同步更新以保持一致的狀態。

+0

不幸的是,我不認爲這會有所幫助。刪除my1,my2,my3以支持共享易失性變量,違背了在while循環迭代過程中(本地)狀態不能改變的要求。雖然保證讀/寫不會使用volatile進行交錯,但並不意味着狀態變量不能被另一個線程在一次迭代中改變。 Esko的解決方案恰好來自Java實踐中的併發。 – heycosmo 2011-06-13 07:01:58

+0

對不起,也許你不是故意刪除my1,my2,my3。然而,在這種情況下,使sh1,sh2,sh3易失性是多餘的,因爲'stateLock'「霧化」了狀態設置和獲取。 – heycosmo 2011-06-13 07:14:38