Java多線程之線程的互斥處理


Java多線程之線程的互斥處理

一、前言

  多線程程序中的各個線程都是自由運行的,所以它們有時就會同時操作同一個實例。這在某些情況下會引發問題。例如,從銀行賬戶取款時,余額確認部分的代碼應該是像下面這樣的。

  if (可用余額大於取款金額) {

    從可用余額中減掉取款金額

  }

  首先確認可用余額,確認是否允許取款。如果允許,則從可用余額上減掉取款金額。這樣才不會導致可用余額變為負數。

  但是,如果兩個線程同時執行這段代碼,那么可用余額就有可能會變為負數。

  假設可用余額=1000元,取款金額= 1000元,那么這種情況就如下圖所示:

  線程A 和線程B 同時操作時,有時線程B 的處理可能會插在線程A 的“可用余額確認”和“從可用余額上減掉取款金額”這兩個處理之間。

  這種線程A 和線程B 之間互相競爭(race)而引起的與預期相反的情況稱為數據競爭(datarace)或競態條件(race condition)。

  這時候就需要有一種“交通管制”來協助防止發生數據競爭。例如,如果一個線程正在執行某一部分操作,那么其他線程就不可以再執行這部分操作。這種類似於交通管制的操作通常稱為互斥(mutual exclusion)。這種處理就像十字路口的紅綠燈,當某一方向為綠燈時,另一方向則一定是紅燈。

  Java 使用關鍵字synchronized 來執行線程的互斥處理。

二、synchronized 方法

  如果聲明一個方法時,在前面加上關鍵字synchronized,那么這個方法就只能由一個線程運行。只能由一個線程運行是每次只能由一個線程運行的意思,並不是說僅能讓某一特定線程運行。這種方法稱為synchronized 方法,有時也稱為同步方法。

  如下所示的類就使用了synchronized 方法。Bank(銀行)類中的deposit(存款)和withdraw(取款)這兩個方法都是synchronized 方法。

  包含deposit 和withdraw 這兩個synchronized 方法的Bank 類(Bank.java)

 1 public class Bank {
 2     private int money;
 3     private String name;
 4 
 5     public Bank(String name, int money) {
 6     this.name = name;
 7     this.money = money;
 8     }
 9 
10     /**
11      * 存款
12      * @param m
13      */
14     public synchronized void deposit(int m) {
15     money += m;
16     }
17 
18     /**
19      * 取款
20      * @param m
21      * @return
22      */
23     public synchronized boolean withdraw(int m) {
24     if (money >= m) {
25         money -= m;
26         return true; // 取款成功
27     } else {
28         return false; // 余額不足
29     }
30     }
31 
32     public String getName() {
33     return name;
34     }
35 }
View Code

   如果有一個線程正在運行Bank 實例中的deposit 方法,那么其他線程就無法運行這個實例中的deposit 方法和withdraw 方法,需要排隊等候。

  Bank 類中還有一個getName 方法。這個方法並不是synchronized 方法,所以無論其他線程是否正在運行deposit 或withdraw,都可以隨時運行getName 方法。

  一個實例中的synchronized 方法每次只能由一個線程運行,而非synchronized 方法則可以同時由兩個以上的線程運行。下圖展示了由兩個線程同時運行getName 方法的情況。

  synchronized 方法不允許同時由多個線程運行。上圖中,我們在synchronized 方法左側放了一個代表“鎖”的長方形來表示這點。當一個線程獲取了該鎖后,長方形這塊兒就像築起的牆一樣,可以防止其他線程進入。

  下圖展示了由一個線程運行deposit 方法的情況。由於該線程獲取了鎖,所以其他線程就無法運行該實例中的synchronized 方法。圖中,表示鎖的長方形被塗成了灰色,這表示該鎖已被某一線程獲取。

  請注意,上圖中,非synchronized 的getName 方法完全不受鎖的影響。不管線程是否已經獲取鎖,都可以自由進入非synchronized 方法。

  當正在使用synchronized 方法的線程運行完這個方法后,便會釋放鎖。下圖中的長方形鎖變為白色表示這個鎖已被釋放。

  當鎖被釋放后,一直等待獲取鎖的線程中的某一個線程便會獲取該鎖。但無論何時,獲取鎖的線程只能是一個。如果等待的線程有很多個,那么沒搶到的線程就只能繼續等待。下圖展示的是新獲取鎖的另一個線程開始運行synchronized 方法的情況。

  每個實例擁有一個獨立的鎖。因此,並不是說某一個實例中的synchronized 方法正在執行中,其他實例中的synchronized 方法就不可以運行了,下圖展示了bank1 和bank2 這兩個實例中的synchronized 方法由不同的線程同時運行的情況。

  • 關於鎖和監視

  線程的互斥機制稱為監視(monitor)。另外,獲取鎖有時也叫作“擁有(own)監視”或“持有(hold)鎖”。

  當前線程是否已獲取某一對象的鎖可以通過Thread.holdsLock 方法來確認。當前線程已獲取對象obj 的鎖時,可使用assert 來像下面這樣表示出來。

  assert Thread.holdsLock(obj);

三、synchronized 代碼塊

  如果只是想讓方法中的某一部分由一個線程運行,而非整個方法,則可使用synchronized代碼塊,格式如下所示。

  synchronized (表達式) {

    .......................

  }

  其中的“表達式”為獲取鎖的實例。synchronized 代碼塊用於精確控制互斥處理的執行范圍。

  ◆◆synchronized實例方法和synchronized代碼塊

  假設有如下synchronized 實例方法。

  synchronized void method() {

    .......................

  }

  這跟下面將方法體用synchronized 代碼塊包圍起來是等效的。

  void method () {

    synchronized (表達式) {

      .......................

    }

  }

  也就是說,synchronized 實例方法是使用this的鎖來執行線程的互斥處理的。

  ◆◆synchronized靜態方法和synchronized代碼塊

  假設有如下synchronized 靜態方法。synchronized 靜態方法每次只能由一個線程運行,這一點和synchronized 實例方法相同。但synchronized 靜態方法使用的鎖和synchronized 實例方法使用的鎖是不一樣的。

  Class Something {

    static synchronized void method() {

      .......................

    }

  }

  這跟下面將方法體用synchronized代碼塊包圍起來是等效的。

  Class Something {

    static  void method() {

      synchronized (Something.class) {

        ...................

      }

    }

  }

  也就是說,synchronized靜態方法是使用該類的類對象的鎖來執行線程的互斥處理的。Something.class是Something 類對應的java.lang.Class 類的實例。

參考:圖解Java多線程設計模式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM