1.解決信號(hào)量丟失和假喚醒
public class MyWaitNotify3{
MonitorObject myMonitorObject = new MonitorObject();
boolean wasSignalled = false;
public void doWait(){
synchronized(myMonitorObject){
while(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}
---------------------------------
饑餓和公平
如果一個(gè)線程因?yàn)?CPU 時(shí)間全部被其他線程搶走而得不到 CPU 運(yùn)行時(shí)間,這種狀態(tài)被稱之為“饑餓”。而該線程被“饑餓致死”正是因?yàn)樗貌坏?CPU 運(yùn)行時(shí)間的機(jī)會(huì)。解決饑餓的方案被稱之為“公平性” – 即所有線程均能公平地獲得運(yùn)行機(jī)會(huì)。
下面是本文討論的主題:
Java 中導(dǎo)致饑餓的原因:
高優(yōu)先級(jí)線程吞噬所有的低優(yōu)先級(jí)線程的 CPU 時(shí)間。
線程被永久堵塞在一個(gè)等待進(jìn)入同步塊的狀態(tài)。
線程在等待一個(gè)本身也處于永久等待完成的對(duì)象(比如調(diào)用這個(gè)對(duì)象的 wait 方法)。
在 Java 中實(shí)現(xiàn)公平性方案,需要:
使用鎖,而不是同步塊。
公平鎖。
注意性能方面。
Java 中導(dǎo)致饑餓的原因
在 Java 中,下面三個(gè)常見的原因會(huì)導(dǎo)致線程饑餓:
高優(yōu)先級(jí)線程吞噬所有的低優(yōu)先級(jí)線程的 CPU 時(shí)間。
線程被永久堵塞在一個(gè)等待進(jìn)入同步塊的狀態(tài),因?yàn)槠渌€程總是能在它之前持續(xù)地對(duì)該同步塊進(jìn)行訪問。
線程在等待一個(gè)本身(在其上調(diào)用 wait())也處于永久等待完成的對(duì)象,因?yàn)槠渌€程總是被持續(xù)地獲得喚醒。
高優(yōu)先級(jí)線程吞噬所有的低優(yōu)先級(jí)線程的 CPU 時(shí)間
你能為每個(gè)線程設(shè)置獨(dú)自的線程優(yōu)先級(jí),優(yōu)先級(jí)越高的線程獲得的 CPU 時(shí)間越多,線程優(yōu)先級(jí)值設(shè)置在 1 到 10 之間,而這些優(yōu)先級(jí)值所表示行為的準(zhǔn)確解釋則依賴于你的應(yīng)用運(yùn)行平臺(tái)。對(duì)大多數(shù)應(yīng)用來說,你最好是不要改變其優(yōu)先級(jí)值。
線程被永久堵塞在一個(gè)等待進(jìn)入同步塊的狀態(tài)
Java 的同步代碼區(qū)也是一個(gè)導(dǎo)致饑餓的因素。Java 的同步代碼區(qū)對(duì)哪個(gè)線程允許進(jìn)入的次序沒有任何保障。這就意味著理論上存在一個(gè)試圖進(jìn)入該同步區(qū)的線程處于被永久堵塞的風(fēng)險(xiǎn),因?yàn)槠渌€程總是能持續(xù)地先于它獲得訪問,這即是“饑餓”問題,而一個(gè)線程被“饑餓致死”正是因?yàn)樗貌坏?CPU 運(yùn)行時(shí)間的機(jī)會(huì)。
線程在等待一個(gè)本身(在其上調(diào)用 wait())也處于永久等待完成的對(duì)象
如果多個(gè)線程處在 wait()方法執(zhí)行上,而對(duì)其調(diào)用 notify()不會(huì)保證哪一個(gè)線程會(huì)獲得喚醒,任何線程都有可能處于繼續(xù)等待的狀態(tài)。因此存在這樣一個(gè)風(fēng)險(xiǎn):一個(gè)等待線程從來得不到喚醒,因?yàn)槠渌却€程總是能被獲得喚醒。
在 Java 中實(shí)現(xiàn)公平性
雖 Java 不可能實(shí)現(xiàn) 100% 的公平性,我們依然可以通過同步結(jié)構(gòu)在線程間實(shí)現(xiàn)公平性的提高。
首先來學(xué)習(xí)一段簡單的同步態(tài)代碼:
public class Synchronizer{
public synchronized void doSynchronized(){
//do a lot of work which takes a long time
}
}
如果有一個(gè)以上的線程調(diào)用 doSynchronized()方法,在第一個(gè)獲得訪問的線程未完成前,其他線程將一直處于阻塞狀態(tài),而且在這種多線程被阻塞的場景下,接下來將是哪個(gè)線程獲得訪問是沒有保障的。
使用鎖方式替代同步塊
為了提高等待線程的公平性,我們使用鎖方式來替代同步塊。
public class Synchronizer{
Lock lock = new Lock();
public void doSynchronized() throws InterruptedException{
this.lock.lock();
//critical section, do a lot of work which takes a long time
this.lock.unlock();
}
}
注意到 doSynchronized()不再聲明為 synchronized,而是用 lock.lock()和 lock.unlock()來替代。
下面是用 Lock 類做的一個(gè)實(shí)現(xiàn):
public class Lock{
private boolean isLocked? ? ? = false;
private Thread lockingThread = null;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
lockingThread = Thread.currentThread();
}
public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException(
"Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
notify();
}
}
注意到上面對(duì) Lock 的實(shí)現(xiàn),如果存在多線程并發(fā)訪問 lock(),這些線程將阻塞在對(duì) lock()方法的訪問上。另外,如果鎖已經(jīng)鎖上(校對(duì)注:這里指的是 isLocked 等于 true 時(shí)),這些線程將阻塞在 while(isLocked)循環(huán)的 wait()調(diào)用里面。要記住的是,當(dāng)線程正在等待進(jìn)入 lock() 時(shí),可以調(diào)用 wait()釋放其鎖實(shí)例對(duì)應(yīng)的同步鎖,使得其他多個(gè)線程可以進(jìn)入 lock()方法,并調(diào)用 wait()方法。
這回看下 doSynchronized(),你會(huì)注意到在 lock()和 unlock()之間的注釋:在這兩個(gè)調(diào)用之間的代碼將運(yùn)行很長一段時(shí)間。進(jìn)一步設(shè)想,這段代碼將長時(shí)間運(yùn)行,和進(jìn)入 lock()并調(diào)用 wait()來比較的話。這意味著大部分時(shí)間用在等待進(jìn)入鎖和進(jìn)入臨界區(qū)的過程是用在 wait()的等待中,而不是被阻塞在試圖進(jìn)入 lock()方法中。
在早些時(shí)候提到過,同步塊不會(huì)對(duì)等待進(jìn)入的多個(gè)線程誰能獲得訪問做任何保障,同樣當(dāng)調(diào)用 notify()時(shí),wait()也不會(huì)做保障一定能喚醒線程(至于為什么,請看線程通信)。因此這個(gè)版本的 Lock 類和 doSynchronized()那個(gè)版本就保障公平性而言,沒有任何區(qū)別。
但我們能改變這種情況。當(dāng)前的 Lock 類版本調(diào)用自己的 wait()方法,如果每個(gè)線程在不同的對(duì)象上調(diào)用 wait(),那么只有一個(gè)線程會(huì)在該對(duì)象上調(diào)用 wait(),Lock 類可以決定哪個(gè)對(duì)象能對(duì)其調(diào)用 notify(),因此能做到有效的選擇喚醒哪個(gè)線程。
公平鎖
下面來講述將上面 Lock 類轉(zhuǎn)變?yōu)楣芥i FairLock。你會(huì)注意到新的實(shí)現(xiàn)和之前的 Lock 類中的同步和 wait()/notify()稍有不同。
準(zhǔn)確地說如何從之前的 Lock 類做到公平鎖的設(shè)計(jì)是一個(gè)漸進(jìn)設(shè)計(jì)的過程,每一步都是在解決上一步的問題而前進(jìn)的:Nested Monitor Lockout, Slipped Conditions 和 Missed Signals。這些本身的討論雖已超出本文的范圍,但其中每一步的內(nèi)容都將會(huì)專題進(jìn)行討論。重要的是,每一個(gè)調(diào)用 lock()的線程都會(huì)進(jìn)入一個(gè)隊(duì)列,當(dāng)解鎖后,只有隊(duì)列里的第一個(gè)線程被允許鎖住 Farlock 實(shí)例,所有其它的線程都將處于等待狀態(tài),直到他們處于隊(duì)列頭部。
public class FairLock {
private boolean? ? ? ? ? isLocked? ? ? = false;
private Thread? ? ? ? ? ? lockingThread? = null;
private List waitingThreads =
new ArrayList();
public void lock() throws InterruptedException{
QueueObject queueObject? ? ? ? ? = new QueueObject();
boolean? ? isLockedForThisThread = true;
synchronized(this){
waitingThreads.add(queueObject);
}
while(isLockedForThisThread){
synchronized(this){
isLockedForThisThread =
isLocked || waitingThreads.get(0) != queueObject;
if(!isLockedForThisThread){
isLocked = true;
waitingThreads.remove(queueObject);
lockingThread = Thread.currentThread();
return;
}
}
try{
queueObject.doWait();
}catch(InterruptedException e){
synchronized(this) { waitingThreads.remove(queueObject); }
throw e;
}
}
}
public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException(
"Calling thread has not locked this lock");
}
isLocked? ? ? = false;
lockingThread = null;
if(waitingThreads.size() > 0){
waitingThreads.get(0).doNotify();
}
}
}
public class QueueObject {
private boolean isNotified = false;
public synchronized void doWait() throws InterruptedException {
while(!isNotified){
this.wait();
}
this.isNotified = false;
}
public synchronized void doNotify() {
this.isNotified = true;
this.notify();
}
public boolean equals(Object o) {
return this == o;
}
}
首先注意到 lock()方法不在聲明為 synchronized,取而代之的是對(duì)必需同步的代碼,在 synchronized 中進(jìn)行嵌套。
FairLock 新創(chuàng)建了一個(gè) QueueObject 的實(shí)例,并對(duì)每個(gè)調(diào)用 lock()的線程進(jìn)行入隊(duì)列。調(diào)用 unlock()的線程將從隊(duì)列頭部獲取 QueueObject,并對(duì)其調(diào)用 doNotify(),以喚醒在該對(duì)象上等待的線程。通過這種方式,在同一時(shí)間僅有一個(gè)等待線程獲得喚醒,而不是所有的等待線程。這也是實(shí)現(xiàn) FairLock 公平性的核心所在。
請注意,在同一個(gè)同步塊中,鎖狀態(tài)依然被檢查和設(shè)置,以避免出現(xiàn)滑漏條件。
還需注意到,QueueObject 實(shí)際是一個(gè) semaphore。doWait()和 doNotify()方法在 QueueObject 中保存著信號(hào)。這樣做以避免一個(gè)線程在調(diào)用 queueObject.doWait()之前被另一個(gè)調(diào)用 unlock()并隨之調(diào)用 queueObject.doNotify()的線程重入,從而導(dǎo)致信號(hào)丟失。queueObject.doWait()調(diào)用放置在 synchronized(this)塊之外,以避免被 monitor 嵌套鎖死,所以另外的線程可以解鎖,只要當(dāng)沒有線程在 lock 方法的 synchronized(this)塊中執(zhí)行即可。
最后,注意到 queueObject.doWait()在 try – catch 塊中是怎樣調(diào)用的。在 InterruptedException 拋出的情況下,線程得以離開 lock(),并需讓它從隊(duì)列中移除。
性能考慮
如果比較 Lock 和 FairLock 類,你會(huì)注意到在 FairLock 類中 lock()和 unlock()還有更多需要深入的地方。這些額外的代碼會(huì)導(dǎo)致 FairLock 的同步機(jī)制實(shí)現(xiàn)比 Lock 要稍微慢些。究竟存在多少影響,還依賴于應(yīng)用在 FairLock 臨界區(qū)執(zhí)行的時(shí)長。執(zhí)行時(shí)長越大,F(xiàn)airLock 帶來的負(fù)擔(dān)影響就越小,當(dāng)然這也和代碼執(zhí)行的頻繁度相關(guān)。
--------------------------
嵌套管程鎖死
嵌套管程鎖死類似于死鎖, 下面是一個(gè)嵌套管程鎖死的場景:
線程 1 獲得 A 對(duì)象的鎖。
線程 1 獲得對(duì)象 B 的鎖(同時(shí)持有對(duì)象 A 的鎖)。
線程 1 決定等待另一個(gè)線程的信號(hào)再繼續(xù)。
線程 1 調(diào)用 B.wait(),從而釋放了 B 對(duì)象上的鎖,但仍然持有對(duì)象 A 的鎖。
線程 2 需要同時(shí)持有對(duì)象 A 和對(duì)象 B 的鎖,才能向線程 1 發(fā)信號(hào)。
線程 2 無法獲得對(duì)象 A 上的鎖,因?yàn)閷?duì)象 A 上的鎖當(dāng)前正被線程 1 持有。
線程 2 一直被阻塞,等待線程 1 釋放對(duì)象 A 上的鎖。
線程 1 一直阻塞,等待線程 2 的信號(hào),因此,不會(huì)釋放對(duì)象 A 上的鎖,
而線程 2 需要對(duì)象 A 上的鎖才能給線程 1 發(fā)信號(hào)……
你可以能會(huì)說,這是個(gè)空想的場景,好吧,讓我們來看看下面這個(gè)比較挫的 Lock 實(shí)現(xiàn):
//lock implementation with nested monitor lockout problem
public class Lock{
protected MonitorObject monitorObject = new MonitorObject();
protected boolean isLocked = false;
public void lock() throws InterruptedException{
synchronized(this){
while(isLocked){
synchronized(this.monitorObject){
this.monitorObject.wait();
}
}
isLocked = true;
}
}
public void unlock(){
synchronized(this){
this.isLocked = false;
synchronized(this.monitorObject){
this.monitorObject.notify();
}
}
}
}
可以看到,lock()方法首先在”this”上同步,然后在 monitorObject 上同步。如果 isLocked 等于 false,因?yàn)榫€程不會(huì)繼續(xù)調(diào)用 monitorObject.wait(),那么一切都沒有問題 。但是如果 isLocked 等于 true,調(diào)用 lock()方法的線程會(huì)在 monitorObject.wait()上阻塞。
這里的問題在于,調(diào)用 monitorObject.wait()方法只釋放了 monitorObject 上的管程對(duì)象,而與”this“關(guān)聯(lián)的管程對(duì)象并沒有釋放。換句話說,這個(gè)剛被阻塞的線程仍然持有”this”上的鎖。
(校對(duì)注:如果一個(gè)線程持有這種 Lock 的時(shí)候另一個(gè)線程執(zhí)行了 lock 操作)當(dāng)一個(gè)已經(jīng)持有這種 Lock 的線程想調(diào)用 unlock(),就會(huì)在 unlock()方法進(jìn)入 synchronized(this)塊時(shí)阻塞。這會(huì)一直阻塞到在 lock()方法中等待的線程離開 synchronized(this)塊。但是,在 unlock 中 isLocked 變?yōu)?false,monitorObject.notify()被執(zhí)行之后,lock()中等待的線程才會(huì)離開 synchronized(this)塊。
簡而言之,在 lock 方法中等待的線程需要其它線程成功調(diào)用 unlock 方法來退出 lock 方法,但是,在 lock()方法離開外層同步塊之前,沒有線程能成功執(zhí)行 unlock()。
結(jié)果就是,任何調(diào)用 lock 方法或 unlock 方法的線程都會(huì)一直阻塞。這就是嵌套管程鎖死。
一個(gè)更現(xiàn)實(shí)的例子
你可能會(huì)說,這么挫的實(shí)現(xiàn)方式我怎么可能會(huì)做呢?你或許不會(huì)在里層的管程對(duì)象上調(diào)用 wait 或 notify 方法,但完全有可能會(huì)在外層的 this 上調(diào)。 有很多類似上面例子的情況。例如,如果你準(zhǔn)備實(shí)現(xiàn)一個(gè)公平鎖。你可能希望每個(gè)線程在它們各自的 QueueObject 上調(diào)用 wait(),這樣就可以每次喚醒一個(gè)線程。
下面是一個(gè)比較挫的公平鎖實(shí)現(xiàn)方式:
//Fair Lock implementation with nested monitor lockout problem
public class FairLock {
private boolean isLocked = false;
private Thread lockingThread = null;
private List waitingThreads =
new ArrayList();
public void lock() throws InterruptedException{
QueueObject queueObject = new QueueObject();
synchronized(this){
waitingThreads.add(queueObject);
while(isLocked ||
waitingThreads.get(0) != queueObject){
synchronized(queueObject){
try{
queueObject.wait();
}catch(InterruptedException e){
waitingThreads.remove(queueObject);
throw e;
}
}
}
waitingThreads.remove(queueObject);
isLocked = true;
lockingThread = Thread.currentThread();
}
}
public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException(
"Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
if(waitingThreads.size() > 0){
QueueObject queueObject = waitingThread.get(0);
synchronized(queueObject){
queueObject.notify();
}
}
}
}
public class QueueObject {}
乍看之下,嗯,很好,但是請注意 lock 方法是怎么調(diào)用 queueObject.wait()的,在方法內(nèi)部有兩個(gè) synchronized 塊,一個(gè)鎖定 this,一個(gè)嵌在上一個(gè) synchronized 塊內(nèi)部,它鎖定的是局部變量 queueObject。
當(dāng)一個(gè)線程調(diào)用 queueObject.wait()方法的時(shí)候,它僅僅釋放的是在 queueObject 對(duì)象實(shí)例的鎖,并沒有釋放”this”上面的鎖。
現(xiàn)在我們還有一個(gè)地方需要特別注意, unlock 方法被聲明成了 synchronized,這就相當(dāng)于一個(gè) synchronized(this)塊。這就意味著,如果一個(gè)線程在 lock()中等待,該線程將持有與 this 關(guān)聯(lián)的管程對(duì)象。所有調(diào)用 unlock()的線程將會(huì)一直保持阻塞,等待著前面那個(gè)已經(jīng)獲得 this 鎖的線程釋放 this 鎖,但這永遠(yuǎn)也發(fā)生不了,因?yàn)橹挥心硞€(gè)線程成功地給 lock()中等待的線程發(fā)送了信號(hào),this 上的鎖才會(huì)釋放,但只有執(zhí)行 unlock()方法才會(huì)發(fā)送這個(gè)信號(hào)。
因此,上面的公平鎖的實(shí)現(xiàn)會(huì)導(dǎo)致嵌套管程鎖死。更好的公平鎖實(shí)現(xiàn)方式可以參考 Starvation and Fairness。
嵌套管程鎖死 VS 死鎖
嵌套管程鎖死與死鎖很像:都是線程最后被一直阻塞著互相等待。
但是兩者又不完全相同。在死鎖中我們已經(jīng)對(duì)死鎖有了個(gè)大概的解釋,死鎖通常是因?yàn)閮蓚€(gè)線程獲取鎖的順序不一致造成的,線程 1 鎖住 A,等待獲取 B,線程 2 已經(jīng)獲取了 B,再等待獲取 A。如避免死鎖中所說的,死鎖可以通過總是以相同的順序獲取鎖來避免。
但是發(fā)生嵌套管程鎖死時(shí)鎖獲取的順序是一致的。線程 1 獲得 A 和 B,然后釋放 B,等待線程 2 的信號(hào)。線程 2 需要同時(shí)獲得 A 和 B,才能向線程 1 發(fā)送信號(hào)。所以,一個(gè)線程在等待喚醒,另一個(gè)線程在等待想要的鎖被釋放。
不同點(diǎn)歸納如下:
死鎖中,二個(gè)線程都在等待對(duì)方釋放鎖。
嵌套管程鎖死中,線程 1 持有鎖 A,同時(shí)等待從線程 2 發(fā)來的信號(hào),線程 2 需要鎖 A 來發(fā)信號(hào)給線程 1。
-----------------------------------
Java 中的讀/寫鎖
相比Java 中的鎖(Locks in Java)里 Lock 實(shí)現(xiàn),讀寫鎖更復(fù)雜一些。假設(shè)你的程序中涉及到對(duì)一些共享資源的讀和寫操作,且寫操作沒有讀操作那么頻繁。在沒有寫操作的時(shí)候,兩個(gè)線程同時(shí)讀一個(gè)資源沒有任何問題,所以應(yīng)該允許多個(gè)線程能在同時(shí)讀取共享資源。但是如果有一個(gè)線程想去寫這些共享資源,就不應(yīng)該再有其它線程對(duì)該資源進(jìn)行讀或?qū)懀?i>譯者注:也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。這就需要一個(gè)讀/寫鎖來解決這個(gè)問題。
Java5 在 java.util.concurrent 包中已經(jīng)包含了讀寫鎖。盡管如此,我們還是應(yīng)該了解其實(shí)現(xiàn)背后的原理。
以下是本文的主題
讀/寫鎖的 Java 實(shí)現(xiàn)(Read / Write Lock Java Implementation)
讀/寫鎖的重入(Read / Write Lock Reentrance)
讀鎖重入(Read Reentrance)
寫鎖重入(Write Reentrance)
讀鎖升級(jí)到寫鎖(Read to Write Reentrance)
寫鎖降級(jí)到讀鎖(Write to Read Reentrance)
可重入的 ReadWriteLock 的完整實(shí)現(xiàn)(Fully Reentrant ReadWriteLock)
在 finally 中調(diào)用 unlock() (Calling unlock() from a finally-clause)
讀/寫鎖的 Java 實(shí)現(xiàn)
先讓我們對(duì)讀寫訪問資源的條件做個(gè)概述:
讀取沒有線程正在做寫操作,且沒有線程在請求寫操作。
寫入沒有線程正在做讀寫操作。
如果某個(gè)線程想要讀取資源,只要沒有線程正在對(duì)該資源進(jìn)行寫操作且沒有線程請求對(duì)該資源的寫操作即可。我們假設(shè)對(duì)寫操作的請求比對(duì)讀操作的請求更重要,就要提升寫請求的優(yōu)先級(jí)。此外,如果讀操作發(fā)生的比較頻繁,我們又沒有提升寫操作的優(yōu)先級(jí),那么就會(huì)產(chǎn)生“饑餓”現(xiàn)象。請求寫操作的線程會(huì)一直阻塞,直到所有的讀線程都從 ReadWriteLock 上解鎖了。如果一直保證新線程的讀操作權(quán)限,那么等待寫操作的線程就會(huì)一直阻塞下去,結(jié)果就是發(fā)生“饑餓”。因此,只有當(dāng)沒有線程正在鎖住 ReadWriteLock 進(jìn)行寫操作,且沒有線程請求該鎖準(zhǔn)備執(zhí)行寫操作時(shí),才能保證讀操作繼續(xù)。
當(dāng)其它線程沒有對(duì)共享資源進(jìn)行讀操作或者寫操作時(shí),某個(gè)線程就有可能獲得該共享資源的寫鎖,進(jìn)而對(duì)共享資源進(jìn)行寫操作。有多少線程請求了寫鎖以及以何種順序請求寫鎖并不重要,除非你想保證寫鎖請求的公平性。
按照上面的敘述,簡單的實(shí)現(xiàn)出一個(gè)讀/寫鎖,代碼如下
public class ReadWriteLock{
private int readers = 0;
private int writers = 0;
private int writeRequests = 0;
public synchronized void lockRead()
throws InterruptedException{
while(writers > 0 || writeRequests > 0){
wait();
}
readers++;
}
public synchronized void unlockRead(){
readers--;
notifyAll();
}
public synchronized void lockWrite()
throws InterruptedException{
writeRequests++;
while(readers > 0 || writers > 0){
wait();
}
writeRequests--;
writers++;
}
public synchronized void unlockWrite()
throws InterruptedException{
writers--;
notifyAll();
}
}
ReadWriteLock 類中,讀鎖和寫鎖各有一個(gè)獲取鎖和釋放鎖的方法。
讀鎖的實(shí)現(xiàn)在 lockRead()中,只要沒有線程擁有寫鎖(writers==0),且沒有線程在請求寫鎖(writeRequests ==0),所有想獲得讀鎖的線程都能成功獲取。
寫鎖的實(shí)現(xiàn)在 lockWrite()中,當(dāng)一個(gè)線程想獲得寫鎖的時(shí)候,首先會(huì)把寫鎖請求數(shù)加 1(writeRequests++),然后再去判斷是否能夠真能獲得寫鎖,當(dāng)沒有線程持有讀鎖(readers==0 ),且沒有線程持有寫鎖(writers==0)時(shí)就能獲得寫鎖。有多少線程在請求寫鎖并無關(guān)系。
需要注意的是,在兩個(gè)釋放鎖的方法(unlockRead,unlockWrite)中,都調(diào)用了 notifyAll 方法,而不是 notify。要解釋這個(gè)原因,我們可以想象下面一種情形:
如果有線程在等待獲取讀鎖,同時(shí)又有線程在等待獲取寫鎖。如果這時(shí)其中一個(gè)等待讀鎖的線程被 notify 方法喚醒,但因?yàn)榇藭r(shí)仍有請求寫鎖的線程存在(writeRequests>0),所以被喚醒的線程會(huì)再次進(jìn)入阻塞狀態(tài)。然而,等待寫鎖的線程一個(gè)也沒被喚醒,就像什么也沒發(fā)生過一樣(譯者注:信號(hào)丟失現(xiàn)象)。如果用的是 notifyAll 方法,所有的線程都會(huì)被喚醒,然后判斷能否獲得其請求的鎖。
用 notifyAll 還有一個(gè)好處。如果有多個(gè)讀線程在等待讀鎖且沒有線程在等待寫鎖時(shí),調(diào)用 unlockWrite()后,所有等待讀鎖的線程都能立馬成功獲取讀鎖 —— 而不是一次只允許一個(gè)。
讀/寫鎖的重入
上面實(shí)現(xiàn)的讀/寫鎖(ReadWriteLock) 是不可重入的,當(dāng)一個(gè)已經(jīng)持有寫鎖的線程再次請求寫鎖時(shí),就會(huì)被阻塞。原因是已經(jīng)有一個(gè)寫線程了——就是它自己。此外,考慮下面的例子:
Thread 1 獲得了讀鎖。
Thread 2 請求寫鎖,但因?yàn)?Thread 1 持有了讀鎖,所以寫鎖請求被阻塞。
Thread 1 再想請求一次讀鎖,但因?yàn)?Thread 2 處于請求寫鎖的狀態(tài),所以想再次獲取讀鎖也會(huì)被阻塞。 上面這種情形使用前面的 ReadWriteLock 就會(huì)被鎖定——一種類似于死鎖的情形。不會(huì)再有線程能夠成功獲取讀鎖或?qū)戞i了。
為了讓 ReadWriteLock 可重入,需要對(duì)它做一些改進(jìn)。下面會(huì)分別處理讀鎖的重入和寫鎖的重入。
讀鎖重入
為了讓 ReadWriteLock 的讀鎖可重入,我們要先為讀鎖重入建立規(guī)則:
要保證某個(gè)線程中的讀鎖可重入,要么滿足獲取讀鎖的條件(沒有寫或?qū)懻埱螅匆呀?jīng)持有讀鎖(不管是否有寫請求)。 要確定一個(gè)線程是否已經(jīng)持有讀鎖,可以用一個(gè) map 來存儲(chǔ)已經(jīng)持有讀鎖的線程以及對(duì)應(yīng)線程獲取讀鎖的次數(shù),當(dāng)需要判斷某個(gè)線程能否獲得讀鎖時(shí),就利用 map 中存儲(chǔ)的數(shù)據(jù)進(jìn)行判斷。下面是方法 lockRead 和 unlockRead 修改后的的代碼:
public class ReadWriteLock{
private Map readingThreads =
new HashMap();
private int writers = 0;
private int writeRequests = 0;
public synchronized void lockRead()
throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(! canGrantReadAccess(callingThread)){
wait();
}
readingThreads.put(callingThread,
(getAccessCount(callingThread) + 1));
}
public synchronized void unlockRead(){
Thread callingThread = Thread.currentThread();
int accessCount = getAccessCount(callingThread);
if(accessCount == 1) {
readingThreads.remove(callingThread);
} else {
readingThreads.put(callingThread, (accessCount -1));
}
notifyAll();
}
private boolean canGrantReadAccess(Thread callingThread){
if(writers > 0) return false;
if(isReader(callingThread) return true;
if(writeRequests > 0) return false;
return true;
}
private int getReadAccessCount(Thread callingThread){
Integer accessCount = readingThreads.get(callingThread);
if(accessCount == null) return 0;
return accessCount.intValue();
}
private boolean isReader(Thread callingThread){
return readingThreads.get(callingThread) != null;
}
}
代碼中我們可以看到,只有在沒有線程擁有寫鎖的情況下才允許讀鎖的重入。此外,重入的讀鎖比寫鎖優(yōu)先級(jí)高。
寫鎖重入
僅當(dāng)一個(gè)線程已經(jīng)持有寫鎖,才允許寫鎖重入(再次獲得寫鎖)。下面是方法 lockWrite 和 unlockWrite 修改后的的代碼。
public class ReadWriteLock{
private Map readingThreads =
new HashMap();
private int writeAccesses? ? = 0;
private int writeRequests? ? = 0;
private Thread writingThread = null;
public synchronized void lockWrite()
throws InterruptedException{
writeRequests++;
Thread callingThread = Thread.currentThread();
while(!canGrantWriteAccess(callingThread)){
wait();
}
writeRequests--;
writeAccesses++;
writingThread = callingThread;
}
public synchronized void unlockWrite()
throws InterruptedException{
writeAccesses--;
if(writeAccesses == 0){
writingThread = null;
}
notifyAll();
}
private boolean canGrantWriteAccess(Thread callingThread){
if(hasReaders()) return false;
if(writingThread == null)? ? return true;
if(!isWriter(callingThread)) return false;
return true;
}
private boolean hasReaders(){
return readingThreads.size() > 0;
}
private boolean isWriter(Thread callingThread){
return writingThread == callingThread;
}
}
注意在確定當(dāng)前線程是否能夠獲取寫鎖的時(shí)候,是如何處理的。
讀鎖升級(jí)到寫鎖
有時(shí),我們希望一個(gè)擁有讀鎖的線程,也能獲得寫鎖。想要允許這樣的操作,要求這個(gè)線程是唯一一個(gè)擁有讀鎖的線程。writeLock()需要做點(diǎn)改動(dòng)來達(dá)到這個(gè)目的:
public class ReadWriteLock{
private Map readingThreads =
new HashMap();
private int writeAccesses? ? = 0;
private int writeRequests? ? = 0;
private Thread writingThread = null;
public synchronized void lockWrite()
throws InterruptedException{
writeRequests++;
Thread callingThread = Thread.currentThread();
while(!canGrantWriteAccess(callingThread)){
wait();
}
writeRequests--;
writeAccesses++;
writingThread = callingThread;
}
public synchronized void unlockWrite() throws InterruptedException{
writeAccesses--;
if(writeAccesses == 0){
writingThread = null;
}
notifyAll();
}
private boolean canGrantWriteAccess(Thread callingThread){
if(isOnlyReader(callingThread)) return true;
if(hasReaders()) return false;
if(writingThread == null) return true;
if(!isWriter(callingThread)) return false;
return true;
}
private boolean hasReaders(){
return readingThreads.size() > 0;
}
private boolean isWriter(Thread callingThread){
return writingThread == callingThread;
}
private boolean isOnlyReader(Thread thread){
return readers == 1 && readingThreads.get(callingThread) != null;
}
}
現(xiàn)在 ReadWriteLock 類就可以從讀鎖升級(jí)到寫鎖了。
寫鎖降級(jí)到讀鎖
有時(shí)擁有寫鎖的線程也希望得到讀鎖。如果一個(gè)線程擁有了寫鎖,那么自然其它線程是不可能擁有讀鎖或?qū)戞i了。所以對(duì)于一個(gè)擁有寫鎖的線程,再獲得讀鎖,是不會(huì)有什么危險(xiǎn)的。我們僅僅需要對(duì)上面 canGrantReadAccess 方法進(jìn)行簡單地修改:
public class ReadWriteLock{
private boolean canGrantReadAccess(Thread callingThread){
if(isWriter(callingThread)) return true;
if(writingThread != null) return false;
if(isReader(callingThread) return true;
if(writeRequests > 0) return false;
return true;
}
}
可重入的 ReadWriteLock 的完整實(shí)現(xiàn)
下面是完整的 ReadWriteLock 實(shí)現(xiàn)。為了便于代碼的閱讀與理解,簡單對(duì)上面的代碼做了重構(gòu)。重構(gòu)后的代碼如下。
public class ReadWriteLock{
private Map readingThreads =
new HashMap();
private int writeAccesses? ? = 0;
private int writeRequests? ? = 0;
private Thread writingThread = null;
public synchronized void lockRead()
throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(! canGrantReadAccess(callingThread)){
wait();
}
readingThreads.put(callingThread,
(getReadAccessCount(callingThread) + 1));
}
private boolean canGrantReadAccess(Thread callingThread){
if(isWriter(callingThread)) return true;
if(hasWriter()) return false;
if(isReader(callingThread)) return true;
if(hasWriteRequests()) return false;
return true;
}
public synchronized void unlockRead(){
Thread callingThread = Thread.currentThread();
if(!isReader(callingThread)){
throw new IllegalMonitorStateException(
"Calling Thread does not" +
" hold a read lock on this ReadWriteLock");
}
int accessCount = getReadAccessCount(callingThread);
if(accessCount == 1){
readingThreads.remove(callingThread);
} else {
readingThreads.put(callingThread, (accessCount -1));
}
notifyAll();
}
public synchronized void lockWrite()
throws InterruptedException{
writeRequests++;
Thread callingThread = Thread.currentThread();
while(!canGrantWriteAccess(callingThread)){
wait();
}
writeRequests--;
writeAccesses++;
writingThread = callingThread;
}
public synchronized void unlockWrite()
throws InterruptedException{
if(!isWriter(Thread.currentThread()){
throw new IllegalMonitorStateException(
"Calling Thread does not" +
" hold the write lock on this ReadWriteLock");
}
writeAccesses--;
if(writeAccesses == 0){
writingThread = null;
}
notifyAll();
}
private boolean canGrantWriteAccess(Thread callingThread){
if(isOnlyReader(callingThread)) return true;
if(hasReaders()) return false;
if(writingThread == null) return true;
if(!isWriter(callingThread)) return false;
return true;
}
private int getReadAccessCount(Thread callingThread){
Integer accessCount = readingThreads.get(callingThread);
if(accessCount == null) return 0;
return accessCount.intValue();
}
private boolean hasReaders(){
return readingThreads.size() > 0;
}
private boolean isReader(Thread callingThread){
return readingThreads.get(callingThread) != null;
}
private boolean isOnlyReader(Thread callingThread){
return readingThreads.size() == 1 &&
readingThreads.get(callingThread) != null;
}
private boolean hasWriter(){
return writingThread != null;
}
private boolean isWriter(Thread callingThread){
return writingThread == callingThread;
}
private boolean hasWriteRequests(){
return this.writeRequests > 0;
}
}
在 finally 中調(diào)用 unlock()
在利用 ReadWriteLock 來保護(hù)臨界區(qū)時(shí),如果臨界區(qū)可能拋出異常,在 finally 塊中調(diào)用 readUnlock()和 writeUnlock()就顯得很重要了。這樣做是為了保證 ReadWriteLock 能被成功解鎖,然后其它線程可以請求到該鎖。這里有個(gè)例子:
lock.lockWrite();
try{
//do critical section code, which may throw exception
} finally {
lock.unlockWrite();
}
上面這樣的代碼結(jié)構(gòu)能夠保證臨界區(qū)中拋出異常時(shí) ReadWriteLock 也會(huì)被釋放。如果 unlockWrite 方法不是在 finally 塊中調(diào)用的,當(dāng)臨界區(qū)拋出了異常時(shí),ReadWriteLock 會(huì)一直保持在寫鎖定狀態(tài),就會(huì)導(dǎo)致所有調(diào)用 lockRead()或 lockWrite()的線程一直阻塞。唯一能夠重新解鎖 ReadWriteLock 的因素可能就是 ReadWriteLock 是可重入的,當(dāng)拋出異常時(shí),這個(gè)線程后續(xù)還可以成功獲取這把鎖,然后執(zhí)行臨界區(qū)以及再次調(diào)用 unlockWrite(),這就會(huì)再次釋放 ReadWriteLock。但是如果該線程后續(xù)不再獲取這把鎖了呢?所以,在 finally 中調(diào)用 unlockWrite 對(duì)寫出健壯代碼是很重要的。
-----------------
重入鎖死
重入鎖死與死鎖和嵌套管程鎖死非常相似。鎖和讀寫鎖兩篇文章中都有涉及到重入鎖死的問題。
當(dāng)一個(gè)線程重新獲取鎖,讀寫鎖或其他不可重入的同步器時(shí),就可能發(fā)生重入鎖死。可重入的意思是線程可以重復(fù)獲得它已經(jīng)持有的鎖。Java 的 synchronized 塊是可重入的。因此下面的代碼是沒問題的:
(譯者注:這里提到的鎖都是指的不可重入的鎖實(shí)現(xiàn),并不是 Java 類庫中的 Lock 與 ReadWriteLock 類)
public class Reentrant{
public synchronized outer(){
inner();
}
public synchronized inner(){
//do something
}
}
注意 outer()和 inner()都聲明為 synchronized,這在 Java 中這相當(dāng)于 synchronized(this)塊(譯者注:這里兩個(gè)方法是實(shí)例方法,synchronized 的實(shí)例方法相當(dāng)于在 this 上加鎖,如果是 static 方法,則不然,更多閱讀:哪個(gè)對(duì)象才是鎖?)。如果某個(gè)線程調(diào)用了 outer(),outer()中的 inner()調(diào)用是沒問題的,因?yàn)閮蓚€(gè)方法都是在同一個(gè)管程對(duì)象(即 this)上同步的。如果一個(gè)線程持有某個(gè)管程對(duì)象上的鎖,那么它就有權(quán)訪問所有在該管程對(duì)象上同步的塊。這就叫可重入。若線程已經(jīng)持有鎖,那么它就可以重復(fù)訪問所有使用該鎖的代碼塊。
下面這個(gè)鎖的實(shí)現(xiàn)是不可重入的:
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
如果一個(gè)線程在兩次調(diào)用 lock()間沒有調(diào)用 unlock()方法,那么第二次調(diào)用 lock()就會(huì)被阻塞,這就出現(xiàn)了重入鎖死。
避免重入鎖死有兩個(gè)選擇:
編寫代碼時(shí)避免再次獲取已經(jīng)持有的鎖
使用可重入鎖
至于哪個(gè)選擇最適合你的項(xiàng)目,得視具體情況而定。可重入鎖通常沒有不可重入鎖那么好的表現(xiàn),而且實(shí)現(xiàn)起來復(fù)雜,但這些情況在你的項(xiàng)目中也許算不上什么問題。無論你的項(xiàng)目用鎖來實(shí)現(xiàn)方便還是不用鎖方便,可重入特性都需要根據(jù)具體問題具體分析。
-------------------------
mport java.util.concurrent.atomic.AtomicLong;
public class AtomicLong{
private AtomicLong count = new AtomicLong(0);
public void inc(){
boolean updated = false;
while(!updated){
long prevCount = this.count.get();
updated = this.count.compareAndSet(prevCount, prevCount + 1);
}
}
public long count(){
return this.count.get();
}
}
這個(gè)版本僅僅是上一個(gè)版本的線程安全版本。這一版我們感興趣的是 inc()方法的實(shí)現(xiàn)。inc()方法中不再含有一個(gè)同步塊。而是被下面這些代碼替代:
boolean updated = false;
while(!updated){
long prevCount = this.count.get();
updated = this.count.compareAndSet(prevCount, prevCount + 1);
}
上面這些代碼并不是一個(gè)原子操作。也就是說,對(duì)于兩個(gè)不同的線程去調(diào)用 inc()方法,然后執(zhí)行 long prevCount = this.count.get()語句,因此獲得了這個(gè)計(jì)數(shù)器的上一個(gè) count。但是,上面的代碼并沒有包含任何的競態(tài)條件。
秘密就在于 while 循環(huán)里的第二行代碼。compareAndSet()方法調(diào)用是一個(gè)原子操作。它用一個(gè)期望值和 AtomicLong 內(nèi)部的值去比較,如果這兩個(gè)值相等,就把 AtomicLong 內(nèi)部值替換為一個(gè)新值。compareAndSet()通常被 CPU 中的 compare-and-swap 指令直接支持。因此,不需要去同步,也不需要去掛起線程。
假設(shè),這個(gè) AtomicLong 的內(nèi)部值是 20。然后,兩個(gè)線程去讀這個(gè)值,都嘗試調(diào)用 compareAndSet(20, 20 + 1)。盡管 compareAndSet()是一個(gè)原子操作,這個(gè)方法也會(huì)被這兩個(gè)線程相繼執(zhí)行(某一個(gè)時(shí)刻只有一個(gè))。
第一個(gè)線程會(huì)使用期望值 20(這個(gè)計(jì)數(shù)器的上一個(gè)值)與 AtomicLong 的內(nèi)部值進(jìn)行比較。由于兩個(gè)值是相等的,AtomicLong 會(huì)更新它的內(nèi)部值至 21(20 + 1 )。變量 updated 被修改為 true,while 循環(huán)結(jié)束。
現(xiàn)在,第二個(gè)線程調(diào)用 compareAndSet(20, 20 + 1)。由于 AtomicLong 的內(nèi)部值不再是 20,這個(gè)調(diào)用將不會(huì)成功。AtomicLong 的值不會(huì)再被修改為 21。變量,updated 被修改為 false,線程將會(huì)再次在 while 循環(huán)外自旋。這段時(shí)間,它會(huì)讀到值 21 并企圖把值更新為 22。如果在此期間沒有其它線程調(diào)用 inc()。第二次迭代將會(huì)成功更新 AtomicLong 的內(nèi)部值到 22。
為什么稱它為樂觀鎖
上一部分展現(xiàn)的代碼被稱為樂觀鎖(optimistic locking)。樂觀鎖區(qū)別于傳統(tǒng)的鎖,有時(shí)也被稱為悲觀鎖。傳統(tǒng)的鎖會(huì)使用同步塊或其他類型的鎖阻塞對(duì)臨界區(qū)域的訪問。一個(gè)同步塊或鎖可能會(huì)導(dǎo)致線程掛起。
樂觀鎖允許所有的線程在不發(fā)生阻塞的情況下創(chuàng)建一份共享內(nèi)存的拷貝。這些線程接下來可能會(huì)對(duì)它們的拷貝進(jìn)行修改,并企圖把它們修改后的版本寫回到共享內(nèi)存中。如果沒有其它線程對(duì)共享內(nèi)存做任何修改, CAS 操作就允許線程將它的變化寫回到共享內(nèi)存中去。如果,另一個(gè)線程已經(jīng)修改了共享內(nèi)存,這個(gè)線程將不得不再次獲得一個(gè)新的拷貝,在新的拷貝上做出修改,并嘗試再次把它們寫回到共享內(nèi)存中去。
稱之為“樂觀鎖”的原因就是,線程獲得它們想修改的數(shù)據(jù)的拷貝并做出修改,在樂觀的假在此期間沒有線程對(duì)共享內(nèi)存做出修改的情況下。當(dāng)這個(gè)樂觀假設(shè)成立時(shí),這個(gè)線程僅僅在無鎖的情況下完成共享內(nèi)存的更新。當(dāng)這個(gè)假設(shè)不成立時(shí),線程所做的工作就會(huì)被丟棄,但任然不使用鎖。
樂觀鎖使用于共享內(nèi)存競用不是非常高的情況。如果共享內(nèi)存上的內(nèi)容非常多,僅僅因?yàn)楦鹿蚕韮?nèi)存失敗,就用浪費(fèi)大量的 CPU 周期用在拷貝和修改上。但是,如果砸共享內(nèi)存上有大量的內(nèi)容,無論如何,你都要把你的代碼設(shè)計(jì)的產(chǎn)生的爭用更低。
樂觀鎖是非阻塞的
我們這里提到的樂觀鎖機(jī)制是非阻塞的。如果一個(gè)線程獲得了一份共享內(nèi)存的拷貝,當(dāng)嘗試修改時(shí),發(fā)生了阻塞,其它線程去訪問這塊內(nèi)存區(qū)域不會(huì)發(fā)生阻塞。
對(duì)于一個(gè)傳統(tǒng)的加鎖/解鎖模式,當(dāng)一個(gè)線程持有一個(gè)鎖時(shí),其它所有的線程都會(huì)一直阻塞直到持有鎖的線程再次釋放掉這個(gè)鎖。如果持有鎖的這個(gè)線程被阻塞在某處,這個(gè)鎖將很長一段時(shí)間不能被釋放,甚至可能一直不能被釋放。
非阻塞算法是不容易實(shí)現(xiàn)的
正確的設(shè)計(jì)和實(shí)現(xiàn)非阻塞算法是不容易的。在嘗試設(shè)計(jì)你的非阻塞算法之前,看一看是否已經(jīng)有人設(shè)計(jì)了一種非阻塞算法正滿足你的需求。
Java 已經(jīng)提供了一些非阻塞實(shí)現(xiàn)(比如 ConcurrentLinkedQueue),相信在 Java 未來的版本中會(huì)帶來更多的非阻塞算法的實(shí)現(xiàn)。
除了 Java 內(nèi)置非阻塞數(shù)據(jù)結(jié)構(gòu)還有很多開源的非阻塞數(shù)據(jù)結(jié)構(gòu)可以使用。例如,LAMX Disrupter 和 Cliff Click 實(shí)現(xiàn)的非阻塞 HashMap。查看我的Java concurrency references page查看更多的資源。
使用非阻塞算法的好處
非阻塞算法和阻塞算法相比有幾個(gè)好處。下面讓我們分別看一下:
選擇
非阻塞算法的第一個(gè)好處是,給了線程一個(gè)選擇當(dāng)它們請求的動(dòng)作不能夠被執(zhí)行時(shí)做些什么。不再是被阻塞在那,請求線程關(guān)于做什么有了一個(gè)選擇。有時(shí)候,一個(gè)線程什么也不能做。在這種情況下,它可以選擇阻塞或自我等待,像這樣把 CPU 的使用權(quán)讓給其它的任務(wù)。不過至少給了請求線程一個(gè)選擇的機(jī)會(huì)。
在一個(gè)單個(gè)的 CPU 系統(tǒng)可能會(huì)掛起一個(gè)不能執(zhí)行請求動(dòng)作的線程,這樣可以讓其它線程獲得 CPU 的使用權(quán)。不過即使在一個(gè)單個(gè)的 CPU 系統(tǒng)阻塞可能導(dǎo)致死鎖,線程饑餓等并發(fā)問題。
沒有死鎖
非阻塞算法的第二個(gè)好處是,一個(gè)線程的掛起不能導(dǎo)致其它線程掛起。這也意味著不會(huì)發(fā)生死鎖。兩個(gè)線程不能互相彼此等待來獲得被對(duì)方持有的鎖。因?yàn)榫€程不會(huì)阻塞當(dāng)它們不能執(zhí)行它們的請求動(dòng)作時(shí),它們不能阻塞互相等待。非阻塞算法任然可能產(chǎn)生活鎖(live lock),兩個(gè)線程一直請求一些動(dòng)作,但一直被告知不能夠被執(zhí)行(因?yàn)槠渌€程的動(dòng)作)。
沒有線程掛起
掛起和恢復(fù)一個(gè)線程的代價(jià)是昂貴的。沒錯(cuò),隨著時(shí)間的推移,操作系統(tǒng)和線程庫已經(jīng)越來越高效,線程掛起和恢復(fù)的成本也不斷降低。不過,線程的掛起和戶對(duì)任然需要付出很高的代價(jià)。
無論什么時(shí)候,一個(gè)線程阻塞,就會(huì)被掛起。因此,引起了線程掛起和恢復(fù)過載。由于使用非阻塞算法線程不會(huì)被掛起,這種過載就不會(huì)發(fā)生。這就意味著 CPU 有可能花更多時(shí)間在執(zhí)行實(shí)際的業(yè)務(wù)邏輯上而不是上下文切換。
在一個(gè)多個(gè) CPU 的系統(tǒng)上,阻塞算法會(huì)對(duì)阻塞算法產(chǎn)生重要的影響。運(yùn)行在 CPUA 上的一個(gè)線程阻塞等待運(yùn)行在 CPU B 上的一個(gè)線程。這就降低了程序天生就具備的并行水平。當(dāng)然,CPU A 可以調(diào)度其他線程去運(yùn)行,但是掛起和激活線程(上下文切換)的代價(jià)是昂貴的。需要掛起的線程越少越好。
降低線程延遲
在這里我們提到的延遲指的是一個(gè)請求產(chǎn)生到線程實(shí)際的執(zhí)行它之間的時(shí)間。因?yàn)樵诜亲枞惴ㄖ芯€程不會(huì)被掛起,它們就不需要付昂貴的,緩慢的線程激活成本。這就意味著當(dāng)一個(gè)請求執(zhí)行時(shí)可以得到更快的響應(yīng),減少它們的響應(yīng)延遲。
非阻塞算法通常忙等待直到請求動(dòng)作可以被執(zhí)行來降低延遲。當(dāng)然,在一個(gè)非阻塞數(shù)據(jù)數(shù)據(jù)結(jié)構(gòu)有著很高的線程爭用的系統(tǒng)中,CPU 可能在它們忙等待期間停止消耗大量的 CPU 周期。這一點(diǎn)需要牢牢記住。非阻塞算法可能不是最好的選擇如果你的數(shù)據(jù)結(jié)構(gòu)哦有著很高的線程爭用。不過,也常常存在通過重構(gòu)你的程序來達(dá)到更低的線程爭用。