Java Thread應該注意的問題 (轉)

worldblog發表於2007-12-12
Java Thread應該注意的問題 (轉)[@more@]

Thread應該注意的問題

(wang hailong)

:namespace prefix = o ns = "urn:schemas--com::office" />

Java的執行緒非常簡單。但有時會看到一些關於執行緒的錯誤用法。下面列出一些應該注意的問題。

1.同步的恆定性

All java s are references.

對於區域性變數和引數來說,java裡面的int, float, double, boolean等基本資料型別,都在棧上。這些基本型別是無法同步的;java裡面的物件(根物件是Object),全都在堆裡,指向物件的reference在棧上。

java中的同步物件,實際上是對於reference所指的“物件地址”進行同步。
需要注意的問題是,千萬不要對同步物件重新賦值。舉個例子。
class A implements Runnable{
  Object lock = new Object();

void run(){
  for(...){
    synchronized(lock){
      // do something
      ...
      lock = new Object();
    }
  }
}

run裡面的這段同步程式碼實際上是毫無意義的。因為每一次lock都給重新分配了新的物件的reference,每個執行緒都在新的reference同步。
大家可能覺得奇怪,怎麼會舉這麼一個例子。因為我見過這樣的程式碼,同步物件在其它的函式里被重新賦了新值。
這種問題很難查出來。
所以,一般應該把同步物件宣告為final.
final Object lock = new Object();

使用Singleton Pattern 設計來獲取同步物件,也是一種很好的選擇。

2.如何放置共享資料

實現執行緒,有兩種方法,一種是繼承Thread類,一種是實現Runnable介面。

上面舉的例子,採用實現Runnable介面的方法。本文推薦這種方法。

首先,把需要共享的資料放在一個實現Runnable介面的類裡面,然後,把這個類的例項傳給多個Thread的構造方法。這樣,新建立的多個Thread,都共同擁有一個Runnable例項,共享同一份資料。

如果採用繼承Thread類的方法,就只好使用static靜態成員了。如果共享的資料比較多,就需要大量的static靜態成員,令資料結構混亂,難以擴充套件。這種情況應該儘量避免。

編寫一段多執行緒程式碼,處理一個稍微複雜點的問題。兩種方法的優劣,一試便知。

3.同步的粒度

執行緒同步的粒度越小越好,即,執行緒同步的程式碼塊越小越好。儘量避免用synchronized修飾符來宣告方法。儘量使用synchronized(anObject)的方式,如果不想引入新的同步物件,使用synchronized(this)的方式。而且,synchronized程式碼塊越小越好。

4.執行緒之間的通知

這裡使用“通知”這個詞,而不用“通訊”這個詞,是為了避免詞義的擴大化。

執行緒之間的通知,透過Object物件的wait()和notify() 或notifyAll() 方法實現。

下面用一個例子,來說明其工作原理:

假設有兩個執行緒,A和B。共同擁有一個同步物件,lock。

1.首先,執行緒A透過synchronized(lock) 獲得lock同步物件,然後lock.wait()函式,放棄lock同步物件,執行緒A停止執行,進入等待佇列。

2.執行緒B透過synchronized(lock) 獲得執行緒A放棄的lock同步物件,做完一定的處理,然後呼叫 lock.notify() 或者lock.notifyAll() 通知等待佇列裡面的執行緒A。

3.執行緒A從等待佇列裡面出來,進入ready佇列,等待排程。

4.執行緒B繼續處理,出了synchronized(lock)塊之後,放棄lock同步物件。

5.執行緒A獲得lock同步物件,繼續執行。

例子程式碼如下:

public class SharedRe implements Runnable{

  Object lock = new Object();

  public void run(){

  // 獲取當前執行緒的名稱。

  String threadName = Thread.currentThread().getName();

 

  if( “A”.equals(threadName)){

  synchronized(lock){ //執行緒A透過synchronized(lock) 獲得lock同步物件

  try{

  System.out.println(“ A gives up lock.”);

  lock.wait(); // 呼叫lock.wait()函式,放棄lock同步物件,

// 執行緒A停止執行,進入等待佇列。

   }catch(InterruptedException e){

  }

  // 執行緒A重新獲得lock同步物件之後,繼續執行。

  System.out.println(“ A got lock again and continue to run.”);

    } // end of synchronized(lock)

  }

  if( “B”.equals(threadName)){

  synchronized(lock){//執行緒B透過synchronized(lock) 獲得執行緒A放棄的lock同步物件

   System.out.println(“B got lock.”);

  lock.notify(); //通知等待佇列裡面的執行緒A,進入ready佇列,等待排程。

  //執行緒B繼續處理,出了synchronized(lock)塊之後,放棄lock同步物件。

   System.out.println(“B gives up lock.”);

    } // end of synchronized(lock)

  boolean hasLock = Thread.holdsLock(lock); // 檢查B是否擁有lock同步物件。

  System.out.println(“B has lock ? -- ” +hasLock); // false.

  }

  }

}

public class TestMain{

  public static void main(){

  Runnable resource = new SharedResource();

  Thread A = new Thread(resource,”A”);

  A.start();

  // 強迫主執行緒停止執行,以便執行緒A開始執行。

  try {

   Thread.sleep(500);

  }catch(InterruptedException e){

  }

  Thread B = new Thread(resource,”B”);

  B.start();

  }

}

5.跨類的同步物件

對於簡單的問題,可以把訪問共享資源的同步程式碼都放在一個類裡面。

但是對於複雜的問題,我們需要把問題分為幾個部分來處理,需要幾個不同的類來處理問題。這時,就需要在不同的類中,共享同步物件。比如,在生產者和消費者之間共享同步物件,在讀者和寫者之間共享同步物件。

如何在不同的類中,共享同步物件。有幾種方法實現,

(1)前面講過的方法,使用static靜態成員,(或者使用Singleton Pattern.)

(2)用引數傳遞的方法,把同步物件傳遞給不同的類。

(3)利用字串常量的“原子性”。

對於第三種方法,這裡做一下解釋。一般來說,程式程式碼中的字串常量經過編譯之後,都具有唯一性,即,中不會存在兩份相同的字串常量。

(通常情況下,C++,C語言程式編譯之後,也具有同樣的特性。)

比如,我們有如下程式碼。

String A = “atom”;

String B = “atom”;

我們有理由認為,A和B指向同一個字串常量。即,A==B。

注意,宣告字串變數的程式碼,不符合上面的規則。

String C= new String(“atom”);

String D = new String(“atom”);

這裡的C和D的宣告是字串變數的宣告,所以,C != D。

有了上述的認識,我們就可以使用字串常量作為同步物件。

比如我們在不同的類中,使用synchronized(“myLock”), “myLock”.wait(),“myLock”.notify(), 這樣的程式碼,就能夠實現不同類之間的執行緒同步。

本文並不強烈推薦這種用法,只是說明,有這樣一種方法存在。

本文推薦第二種方法,(2)用引數傳遞的方法,把同步物件傳遞給不同的類。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-992318/,如需轉載,請註明出處,否則將追究法律責任。

相關文章