由於功率牆的影響,現代 CPU 傾向於使用多個核心(core)來提高其整體性能。這意味着,軟件開發人員不再能夠像以前一樣,把軟件放兩年,再拿出來,它的性能就變得足夠好了。爲了充分利用多核 CPU 的能力,我們也必須進入多線程編程的世界。
對 Java 程序員來說,這不是一件太困難的事。我們的語言本來就內置了同步功能。其中最常用的,莫過於 synchronized
關鍵字。他一共有兩種用法:
a. synchronized
語句
synchronized (mLock) {
// Put your stuff here
}
b. synchronized
方法
public synchronized void foo() {
// Put your stuff here
}
synchronized
加鎖時,使用的是對象的內置鎖(intrinsic lock),任何 Java 對象都擁有這個鎖,所以任何 Java 對象都可以用來給 synchronized
執行同步。synchronized
語句使用的對象由我們顯式指定,synchronized
方法則用的是方法所屬的那個對象的內置鎖。
我們知道,靜態方法不屬於任何對象,那靜態的 synchronized
方法又用的是哪個鎖呢?
回想一下,剛剛我提到,任何 Java 對象都可以用來給 synchronized
執行同步。雖然靜態方法不關聯對象,但是,他卻屬於所在的那個類實例。下面我們通過例子說明一下:
class Foo {
public static synchronized void foo() {}
}
這裏的 foo()
屬於 class Foo
。我們又知道,Java 裏,class Foo
對應着這樣一個類實例 Foo.class
。Foo.class
也是一個對象,所以他能夠被用來加鎖。靜態的同步方法,就是使用對應的類實例來進行加鎖的。
同樣,對應靜態的代碼塊,我們也可以這樣做:
static {
synchronized (Foo.class) {
// your stuff...
}
}
當然,更推薦的做法是(原因見《Effective Java》):
private static final Object sLock = new Object();
static {
synchronized (sLock) {
// your stuff...
}
}
上面說完了 synchronized
的一些基本用法,下面講講和它相關的 wait()
和 notify(), notifyAll()
。
在某些情況下,我們可能要先獲取鎖,然後檢查某些條件,如果條件不滿足,則放棄鎖,稍後再重試。
// some thread
synchronized (mLock) {
while (!condition) {
wait();
}
}
// another thread
synchronized (mLock) {
condition = true;
notify();
// or notifyAll();
}
這個時候, wait()
和 notify(), notifyAll()
就派上用場了。執行 wait()
後,會釋放鎖,然後進入休眠。稍後,其他某個線程修改條件,並重新喚醒前面那個線程。先前調用 wait()
的線程被喚醒後,會自動重新獲得鎖。也就是說,wait()
調用返回後,該線程仍然持有鎖。
注:某些面試官喜歡讓面試者回答 wait()
和 sleep()
的區別。sleep()
是 class Thread
的一個方法,它所做的就是直接去睡覺(不釋放鎖),兩者的區別是非常明顯的。
瞭解了基本的用法後,我們現在來看看 synchronized
背地裏做了什麼。
首先,我們知道,synchronized
所實現的範式稱爲管程(monitor,操作系統的教科書一般都會介紹)。所謂的管程,簡單講就是某一段代碼,同一時間只有一個線程可以執行它。
Java 所實現的管程是可重入的。意思是,只要某個獲得鎖,它就可以重複地獲得這個鎖,就像下面的代碼這樣:
synchronized void foo() {
bar();
}
synchronized void bar() {
}
這裏,我們在進入 foo()
後就已經獲得了鎖,調用 bar()
的時候,我們又再獲取了一次。與可重入鎖相對於的,是不可重入鎖。著名的 pthread
庫所實現的 mutex
默認就是不可重入的,C++ 的 std::mutex
也一樣。在上面的例子中,對於不可重入鎖,在 foo()
裏調用 bar()
將會導致死鎖。
接下來,我們聊聊條件隊列(condition queue)。Java 的 API 並沒有暴露出條件隊列這個東西,但是,瞭解它會讓我們更清楚 wait(), notify(),notifyAll()
的作用。
首先,條件隊列是一個等待隊列,每個 condition
都關聯着一個條件隊列。如果使用的是內置鎖(也就是 synchronized
這種方式),一個對象只有一個 condition
。如果需要多個,我們就得使用 class ReentrantLock
。
我們調用 wait()
後,調用線程會將自己放到這個內置鎖對應的條件隊列上,然後釋放鎖並休眠。當另一個線程調用 notify()
的時候,就會喚醒這個條件隊列上的某一個線程(具體是哪一個依賴於實現)。被喚醒的線程則重新嘗試獲取鎖,如果成功了,wait()
調用就會返回。如果一直沒有人 notify()
它,它將會永遠處於休眠狀態。
這個時候,你應該可以猜到,notifyAll()
的作用就是喚醒條件隊列裏所有的線程。之所以某些時候 notifyAll()
會帶來性能問題,也是這個特性導致的。
想象一種極端的情況,我們有 100 個線程消費者線程,1 個生產者線程。消費者在沒有物品消費的時候,就調用 wait()
在條件隊列上等待。由於生產非常慢,100 個消費者在檢查條件後,發現都不滿足,於是都在條件隊列上休眠。過了一段時間,生產者終於生產出了一個物品,然後調用 nofityAll()
。結果,100 個消費者都醒了過來,可是最後只有 1 個能夠拿到這個產品,其餘 99 個線程什麼工作都沒有做,就又得回到休眠狀態。這種情況下,使用 nofity()
會更加的高效。需要注意的是,nofity()
在某些情況下卻會導致死鎖,所以只有在經過精細地設計後,才能使用 nofity()
。
總的來講,一開始應該總是使用 notifyAll()
,只有在發現確實它導致性能問題時,才考慮 notify()
,並且對死鎖問題給予足夠的關注。
注:notify()
喚醒哪個線程依賴實現指的是,多個線程去搶一個鎖,我們不知道哪一個線程會先執行,(檢查條件不滿足)然後先放到條件隊列裏面。雖然我們叫他條件隊列,但實現不一定就是隊列。假設實現是一個 list,那我們 notify 的時候,也不知道他會 notify 隊頭元素還是隊尾。