2012年10月18日 星期四

synchronized、wait() 和 notify() 的觀念與用法 (待續)

在多執行緒的環境中,有時會產生需要造就出類似資料庫的交易環境
也就是某個一系列的動作正在執行時,必須確保執行期間的資料不會被別的執行緒修改。
例如有某個變數 a 的當前數值是 0,然後同時有兩個執行緒 T#1、T#2 進來要存取 a
T#1 進行 a++ 的動作,接著把 a++ 的結果寫回 a
但寫回去之前,T#2 已經讀取了當前的 a(即讀取到 a=0),並且同樣嘗試要做 a++
T#1 寫回去以後,a=1,接著 T#2 又寫回去,a 仍然等於 1
這時就造成資料不一致,亦即 Race condition [1] 的問題發生了
因為理想上 T#1、T#2 個別對 a 做了 a++,應該要產生 a=2 的結果。

經典的例子可以參考 [2] 裡面的例子,以下轉錄該例子的原始碼
static int x = 0, y = 0, a = 0, b = 0;

public static void main(String[] args) throws Exception {
  for (int i = 0; i < 100; i++) {
    x = y = a = b = 0;
    Thread one = new Thread() {
      public void run() {
        a = 1;
        x = b;
      }
    };
    Thread two = new Thread() {
      public void run() {
        b = 1;
        y = a;
      }
    };
    one.start();
    two.start();
    one.join();
    two.join();
    System.out.println(x + " " + y);
  }
}
可以看出實際上要執行的就是指定 a、b 都是 1,然後交叉指定,在 Thread 1 中把 b 指給 x、Thread 2 中把 a 指給 y
最後印出 x 和 y 看看最終交叉指定的結果是怎麼樣。
如果完全按照呼叫的順序的話,應該會是等同於以下的程式碼:
a = 1;
x = b;
b = 1;
y = a;
也就是 (x,y) 的結果應該會是 (0, 1)~
但實際執行時,卻不一定是如此,有時會出現 (1, 0)、有時會是 (0, 1)
這就是發生在有執行緒在讀取了例如 b 之後,設定 b=1,但在執行緒還沒有寫入 b=1 之前,另一個執行緒已經去讀 b 了
於是執行緒 #2 讀到的 b 是 b=0,然後執行緒 #1 才把 b=1 寫入。

在 Java 中,要處理這種共時的問題,基本上是有兩種方法(吧 XD)
一種是利用 java.util.concurrent 套件裡面的各種 API(簡介可以參考看看 [3])
另一種就是利用 synchronized 關鍵字以及 wait()、notify() 機制。

wait()、notify() 機制的概念大概是說,每個被 synchronized 的物件都會有一個 monitor
每個執行緒要先取得物件的 monitor,才可以對物件進行操作
而 wait() 的動作就是讓執行緒去該物件的 monitor 排隊~
當物件的 notify() 被呼叫時,JVM 會自動找出並喚醒 monitor 上下一個正在排隊的執行緒
該執行緒取得 monitor 後,就會開始執行 wait() 下一行開始的動作。

PS. notify() 被呼叫時,JVM 似乎不一定是完全按照順序,但總之它會喚醒某一個正在排隊的執行緒。

比較完整的說明可以參考 [4],以下節錄它的 Part 1 一開始的說明:
Every Java class extends the Object class by default. There are several built-in methods on this class, of which we'll focus on wait(), notify(), and notifyAll(). These methods operate on the object's monitor, which is used to synchronize operations across a group of threads. There are some rules to follow, but it's really quite simple: The waiting thread needs to own the object's monitor, which means it must call wait() within the code that synchronizes on the monitor.
參考資料:
1、Race condition
2、深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则
3、浅谈java.util.concurrent包的并发处理
4、Java Concurrency: Queue Processing Part 1, Part 2
5、Java:使用wait()与notify()实现线程间协作

沒有留言: