Practical Java(重點版)之多線程

 

1. 面對instance 函數,synchronized 鎖定的是對象(objects)而非函數(methods)或代碼(code)。

   Synchronized既可以作用於方法修飾,也可以用於方法內的修飾。對於instance 函數,關鍵詞synchronized 其實並不鎖定方法或代碼,它鎖定的是對象(至於synchronized 對statics 的影響,請見下面)。記住,每個對象只有一個lock(機鎖)與之相關聯。當synchronized 被當作函數修飾符的時候,它所取得的lock 將被交給函數調用者(某對象)。如果synchronized 用於object reference,則取得的lock 將被交給該reference所指對象。分析如下:

public class Test {

   public synchronized void method1() {//修飾方法

 

   }

 

   public void method2() {//修飾object reference

      synchronized (this) {

      }

   }

  

   public void method3(Object object) {//修飾object reference

      synchronized (object) {

      }

   }

}

前兩個函數method1()和method2()在[對象鎖定]方面功能一致。 二者都對this進行同步控制。換句話說,獲得的lock 將給予調用此函數的對象(也就是this)。由於這兩個函數都隸屬class Test,所以lock 由Test 的某個對象獲得。method3()則同步控制object所指的那個對象。

對一個對象進行同步控制到底意味什麼呢?它意味[調用該函數]之線程將會取得對象的lock。持有[對象A 之lock],意味另一個通過synchronized 函數或synchronized語句來申請[對象A 之lock]的線程,在該lock 被釋放之前將無法獲得滿足。然而如果另一個線程對對象A 所屬類之另一對象B 調用相同的synchronized 函數或synchronized 區塊,可以獲得[對象B 之lock]。因此,synchronized 函數或synchronized 區段內的代碼在同一時刻下可由多個線程執行,只要是對不同的對象調用該函數。

記住,同步機制(synchronization)鎖定的是對象,而不是函數或代碼。函數或代碼區段被聲明爲synchronization 並非意味它在同一時刻只能由一個線程執行。

最後一點說明是:Java 語言不允許你將構造函數聲明爲,synchronized(那麼做會發生編譯錯誤)。原因是當兩個線程併發調用同一個構造函數時,它們各自操控的是同一個class 的兩個不同實體(對象)的內存(也就是說synchronized 是畫蛇添足之舉)。然而如果在這些構造函數內包含了彼此競爭共享資源的代碼(比如說靜態變量),則必須同步控制那些資源以迴避衝突。

2. 弄滇楚synchronized statics函數(同步靜態函數)與synchronized instance函數(同步實例函數)之間的差異。

當調用synchronized statics方法時,獲取到的lock是與定義該方法的Class對象相關,而不是與調用該方法的對象有關。當你對一個class literal(類名稱字面常量)調用其synchronized 區段時,獲得的也是同樣那個lock,也就是[與特定Class 對象相關聯]的lock。

如下:

public class Thread01 {

   public static synchronized void method() {

 

   }

 

   public void method1() {

      synchronized (Thread01.class) {

 

      }

   }

}

method()和method1()都是爭取的同一個lock,也就是Thread01 Class object lock。method()通過synchronized的修飾符來獲取lock,而method2()是通過class literal Thread01.class來獲取lock的。

如果synchronized 施行於instance 函數和object references,得到的lock 就與前面的不一樣了。對於instance 函數,取得的lock 隸屬於其調用者(某個對象),至於同步控制一個(指名)對象,取得的當然是該對象的lock。

由於同步控制(1)instance 函數(2)static 函數(3)對象(object) (4)class literals 時得到的locks 不同,因此在決定互(mutual exclusion)行爲時一定要小心謹慎。記住,同步控制[通過instance 函數或object reference 所取得的lock]。完全不同於同步控制[通過static 函數或class literal 所取得的lock]。兩個函數被聲明爲synchronized 並不就意味它們具備多線程安全性。你必須小心識別和區分通過同步控制所取得的locks 之間的微妙差異。

如下:

class Thread02 implements Runnable {

 

   public synchronized void printlnM1() {

      while (true) {

        System.out.println("printlnM1");

      }

   }

 

   public static synchronized void printlnM2() {

      while (true) {

        System.out.println("printlnM2");

      }

   }

 

   @Override

   public void run() {

      printlnM1();

   }

}

 

class TestThread {

   public static void main(String[] args) {

      Thread02 t = new Thread02();

      Thread f = new Thread(t);

      f.start();

      t.printlnM2();

   }

}

儘管上述兩個函數都聲明爲synchronized,它們並非[多線程安全](thread safe)。

其原因在於一個是synchronized static 函數,另一個是synchronized instance函數。因此它們爭取的是不同的locks。instance 函數printlnM1()取得的是Thread02 object lock,static 函數printlnM2()取得的是Thread02 的Class object lock。這是不同的兩個locks,彼此互不影響。當上述代碼執行起來,兩個字符串都打印在屏幕上。換句話講,兩個函數的執行交互穿插。如果需要同步控制這段代碼,可以共享同一個資源。爲了保護這筆資源,代碼必須有正確的同步控制,以避免衝突。兩種選擇可以解決這個問題:

1.同步控制(synchronize)公用資源。

2. 同步控制(synchronize)—個特殊的instance 變量。

方案一實現:前提假設兩個函數要更新同一個對象,它們就對其進行同步控制。

  class Thread02 implements Runnable {

   private Object o;

 

   public synchronized void printlnM1() {

      synchronized (o) {

        while (true) {

           System.out.println("printlnM1");

        }

      }

   }

 

   public static synchronized void printlnM2(Thread02 o) {

      synchronized (o.o) {

        while (true) {

           System.out.println("printlnM2");

        }

      }

   }

 

   @Override

   public void run() {

      printlnM1();

   }

}

方案二實現:聲明一個local instance 變量,惟一的目的就是對它進行同步控制。

class Thread02 implements Runnable {

   private byte[] lock = new byte[0];

 

   public synchronized void printlnM1() {

      synchronized (lock) {

        while (true) {

           System.out.println("printlnM1");

        }

      }

   }

 

   public static synchronized void printlnM2(Thread02 o) {

      synchronized (o.lock) {

        while (true) {

           System.out.println("printlnM2");

        }

      }

   }

 

   @Override

   public void run() {

      printlnM1();

   }

}由於只能鎖定對象,你使用的local instance 變量必須是個對象。

3. 以[private數據+相應的訪問函數(accessor)]替換[public/protected數據。這樣做是爲了封裝。記住,對於[在synchronized 函數中可被修改的數據],應使之成爲private,並根據需要提供訪問函數(accessor)。如果訪問函數返回是可變對象(mutable object),那麼應該先cloned(克隆)該對象。

4. 要避免無所謂的同步控制。過度的同步控制估計會的導致死鎖(deadlocks)或者併發(concurrency)度降低。同步機制對每個對象只提供一個lock。當一個函數聲明爲synchronized,所獲得的lock 乃是隸屬於調用此函數的那個對象。如果該對象要訪問其他方法的話,就必須釋放lock,這樣的話性能就降低了,這時可以再添加一個變量來產生不同的lock。如下:

class Thread03{

      private int[] a1;

  private int[] a2;

  private double[] d1;

      private double[] d2;

 

  private byte[] a = new byte[0];

  private byte[] d = new byte[0];

 

  public void ma1(){

      synchronized (a) {

       

     }

  }

  

      public void ma2(){

      synchronized (a) {

       

     }

  }

 

      public void md1(){

     synchronized (d) {

       

     }

  }

  public void md2(){

     synchronized (d) {

      

      }

}

}

 

 

5. 訪問共享變量時請使用synchronized或volatile。可以確保變量與主內存完全保持一致,從而在任何時候得到正確數值。記住,一旦變量被聲明爲volatile,在每次訪問它們時,它們就與主內存進行一致化。但如果使用synchronized,只有在取得lock 和釋放lock 的時候,纔會對變量和主內存進行一致化。

 

優點

缺點

synchronized

取得和釋放lock 時,進行私

有專用副本

與主內存正本的一致化

消除了併發性的可能

volatile

允許併發

每次訪問變量,就進行稀有專用內存與對

應之主內存的一致化

 

6. 在單一操作中鎖定所有用到的對象。

7. 以固定而全局性的順序取得多個locks(機鎖)以避免死鎖。

死鎖:當兩個或者多個線程因爲互相等待而阻塞。

8. 優先使用notifyAll()而非notify()。

9. 針對wait()和notifyAll()使用旋鎖(spin locks)。只在代碼等待着某個特定條件,它就應當在一個循環內(或謂旋鎖,spin lock)做那件事(那件事指的是[等待着某個特定條件])。尤其是在判斷null之類的時候,因爲不能夠確保多個線程被喚醒的時候,其條件是否爲空。

10.  使用wait()和notifyAll()替換輪詢循環(polling loops)。

11. 不要對locked object(上鎖對象)之object reference重新賦值。

12. 不要調用stop()或suspend()。

Stop()的本意是用來中止一個線程。中止線程的問題根源不在object locks,而在object 的狀態。當stop()中止一個線程時,會釋放線程持有的所有locks。但是你並不知道當時代碼正在做什麼。

Suspend()的本意時用來[暫時懸掛起一個線程](它由一個對應的resume()函數,用來恢復先前被懸掛起來的線程。Resume()也不再獲得Java 2 SDK 支持)。Suspend()同樣時不安全的,但其原因和stop()的故事不同。和stop()不同,suspend()並不釋放[即將被懸掛支線程說持有的locks]。這些locks 在線程恢復執行前永遠不會釋放。

stop()帶來[攪亂內部數據]的風險,suspend()帶來死鎖的風險。

13. 通過線程(threads)之間的協作來中止線程。

如何中止線程呢?在class 內提供一個變量,以及一個用來設置此變量值的函數。該變量用來表示線程何時應該被中止。

class Thread04 extends Thread {

   private volatile boolean stop;

 

   public void stopFlag() {

     stop = true;

   }

 

   @Override

   public void run() {

     while (stop) {

        super.run();

     }

   }

}

 

發佈了42 篇原創文章 · 獲贊 0 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章