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 }
如果有一個線程正在運行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多線程設計模式