java并發(fā)與多線程

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á)到更低的線程爭用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,566評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,683評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,899評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,135評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評(píng)論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,528評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,844評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容