網頁

2017/10/4

Java Thread Interference 執行緒干涉

Thread Interference是指多個執行緒執行時同時存取同一筆資料,在非執行緒安全的程式可能會造成非預期的結果。

下面範例有兩個執行緒Thread-a及Thread-b執行時共同存取一個用來計數的Counter

public class Counter {

  private int c = 0;
  
  public void increment(){
    c++;
  }
  public void decrement(){
    c--;
  }
  public int getCount(){
    return c;
  }

}

public class Test {

  public static void main(String[] args) {
    Counter counter = new Counter();
    Thread a = new Thread(new Runnable(){
      @Override
      public void run() {
        counter.increment();
        System.out.println(Thread.currentThread().getName() + ":" + counter.getCount());
      }
    });
    a.setName("Thread-a");
    
    Thread b = new Thread(new Runnable(){
      @Override
      public void run() {
        counter.decrement();
        System.out.println(Thread.currentThread().getName() + ":" + counter.getCount());
      }
    });
    b.setName("Thread-b");
    
    a.start();
    b.start();
    
  }
  
}

但卻有可能出現下面的執行結果。

Thread-a:0
Thread-b:0

執行緒的執行順序是由作業系統決定,當Thread-a和Thread-b呼叫start()開始執行後,

  1. 先執行了Thread-a的counter.increment(),此時的c的值為1。
  2. 但接著又換成Thread-b執行counter.decrement(),此時的c為0,
  3. 接著又切回Thread-a執行印出c的值,所以印出Thread-a:0,
  4. 然後切到Thread-b,同樣的也是印出Thread-b:0

這種多執行緒交錯執行又同時存取同一資料造成非預期的結果的現象就稱為Thread Interference。

要避免Thread Interference,可以使用Java的synchronized關鍵字來將物件同步,例如宣告一個同步區塊synchronized block並傳入要鎖定(lock)的物件,在區塊內該物件會被鎖定直到執行完離開區塊為止,在區塊內鎖定的物件只會被一條執行緒存取。

例如將上面Counter範例加上synchronized如下。將counter物件傳入synchronized block。

public class Test {

  public static void main(String[] args) {
    Counter counter = new Counter();
    Thread a = new Thread(new Runnable(){
      @Override
      public void run() {
        synchronized(counter){
          counter.increment();
          System.out.println(Thread.currentThread().getName() + ":" + counter.getCount());
          
        }
      }
    });
    a.setName("Thread-a");
    
    Thread b = new Thread(new Runnable(){
      @Override
      public void run() {
        synchronized(counter){
          counter.decrement();
          System.out.println(Thread.currentThread().getName() + ":" + counter.getCount());
        }
      }
    });
    b.setName("Thread-b");
    
    a.start();
    b.start();
    
  }
  
}

當Thread-a及Thread-b開始執行後,

  1. a.start()先被呼叫,所以先進入Thread-a的run()中並馬上進入synchronized block,
  2. 在區塊中counter物件只會被Thread-a存取並執行到離開區塊後才會解除鎖定,
  3. 接著才開始執行Thread-b的run()直到結束。


在多執行緒的環境中若要對數值進行累加累減的運算,應使用AtomicInteger原子數值類來操作。

參考:

沒有留言:

張貼留言