java并發與多線程

1.解決信號量丟失和假喚醒

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();

}

}

}

---------------------------------

饑餓和公平

如果一個線程因為 CPU 時間全部被其他線程搶走而得不到 CPU 運行時間,這種狀態被稱之為“饑餓”。而該線程被“饑餓致死”正是因為它得不到 CPU 運行時間的機會。解決饑餓的方案被稱之為“公平性” – 即所有線程均能公平地獲得運行機會。

下面是本文討論的主題:

Java 中導致饑餓的原因:

高優先級線程吞噬所有的低優先級線程的 CPU 時間。

線程被永久堵塞在一個等待進入同步塊的狀態。

線程在等待一個本身也處于永久等待完成的對象(比如調用這個對象的 wait 方法)。

在 Java 中實現公平性方案,需要:

使用鎖,而不是同步塊。

公平鎖。

注意性能方面。

Java 中導致饑餓的原因

在 Java 中,下面三個常見的原因會導致線程饑餓:

高優先級線程吞噬所有的低優先級線程的 CPU 時間。

線程被永久堵塞在一個等待進入同步塊的狀態,因為其他線程總是能在它之前持續地對該同步塊進行訪問。

線程在等待一個本身(在其上調用 wait())也處于永久等待完成的對象,因為其他線程總是被持續地獲得喚醒。

高優先級線程吞噬所有的低優先級線程的 CPU 時間

你能為每個線程設置獨自的線程優先級,優先級越高的線程獲得的 CPU 時間越多,線程優先級值設置在 1 到 10 之間,而這些優先級值所表示行為的準確解釋則依賴于你的應用運行平臺。對大多數應用來說,你最好是不要改變其優先級值。

線程被永久堵塞在一個等待進入同步塊的狀態

Java 的同步代碼區也是一個導致饑餓的因素。Java 的同步代碼區對哪個線程允許進入的次序沒有任何保障。這就意味著理論上存在一個試圖進入該同步區的線程處于被永久堵塞的風險,因為其他線程總是能持續地先于它獲得訪問,這即是“饑餓”問題,而一個線程被“饑餓致死”正是因為它得不到 CPU 運行時間的機會。

線程在等待一個本身(在其上調用 wait())也處于永久等待完成的對象

如果多個線程處在 wait()方法執行上,而對其調用 notify()不會保證哪一個線程會獲得喚醒,任何線程都有可能處于繼續等待的狀態。因此存在這樣一個風險:一個等待線程從來得不到喚醒,因為其他等待線程總是能被獲得喚醒。

在 Java 中實現公平性

雖 Java 不可能實現 100% 的公平性,我們依然可以通過同步結構在線程間實現公平性的提高。

首先來學習一段簡單的同步態代碼:

public class Synchronizer{

public synchronized void doSynchronized(){

//do a lot of work which takes a long time

}

}

如果有一個以上的線程調用 doSynchronized()方法,在第一個獲得訪問的線程未完成前,其他線程將一直處于阻塞狀態,而且在這種多線程被阻塞的場景下,接下來將是哪個線程獲得訪問是沒有保障的。

使用鎖方式替代同步塊

為了提高等待線程的公平性,我們使用鎖方式來替代同步塊。

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 類做的一個實現:

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();

}

}

注意到上面對 Lock 的實現,如果存在多線程并發訪問 lock(),這些線程將阻塞在對 lock()方法的訪問上。另外,如果鎖已經鎖上(校對注:這里指的是 isLocked 等于 true 時),這些線程將阻塞在 while(isLocked)循環的 wait()調用里面。要記住的是,當線程正在等待進入 lock() 時,可以調用 wait()釋放其鎖實例對應的同步鎖,使得其他多個線程可以進入 lock()方法,并調用 wait()方法。

這回看下 doSynchronized(),你會注意到在 lock()和 unlock()之間的注釋:在這兩個調用之間的代碼將運行很長一段時間。進一步設想,這段代碼將長時間運行,和進入 lock()并調用 wait()來比較的話。這意味著大部分時間用在等待進入鎖和進入臨界區的過程是用在 wait()的等待中,而不是被阻塞在試圖進入 lock()方法中。

在早些時候提到過,同步塊不會對等待進入的多個線程誰能獲得訪問做任何保障,同樣當調用 notify()時,wait()也不會做保障一定能喚醒線程(至于為什么,請看線程通信)。因此這個版本的 Lock 類和 doSynchronized()那個版本就保障公平性而言,沒有任何區別。

但我們能改變這種情況。當前的 Lock 類版本調用自己的 wait()方法,如果每個線程在不同的對象上調用 wait(),那么只有一個線程會在該對象上調用 wait(),Lock 類可以決定哪個對象能對其調用 notify(),因此能做到有效的選擇喚醒哪個線程。

公平鎖

下面來講述將上面 Lock 類轉變為公平鎖 FairLock。你會注意到新的實現和之前的 Lock 類中的同步和 wait()/notify()稍有不同。

準確地說如何從之前的 Lock 類做到公平鎖的設計是一個漸進設計的過程,每一步都是在解決上一步的問題而前進的:Nested Monitor Lockout, Slipped Conditions 和 Missed Signals。這些本身的討論雖已超出本文的范圍,但其中每一步的內容都將會專題進行討論。重要的是,每一個調用 lock()的線程都會進入一個隊列,當解鎖后,只有隊列里的第一個線程被允許鎖住 Farlock 實例,所有其它的線程都將處于等待狀態,直到他們處于隊列頭部。

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,取而代之的是對必需同步的代碼,在 synchronized 中進行嵌套。

FairLock 新創建了一個 QueueObject 的實例,并對每個調用 lock()的線程進行入隊列。調用 unlock()的線程將從隊列頭部獲取 QueueObject,并對其調用 doNotify(),以喚醒在該對象上等待的線程。通過這種方式,在同一時間僅有一個等待線程獲得喚醒,而不是所有的等待線程。這也是實現 FairLock 公平性的核心所在。

請注意,在同一個同步塊中,鎖狀態依然被檢查和設置,以避免出現滑漏條件。

還需注意到,QueueObject 實際是一個 semaphore。doWait()和 doNotify()方法在 QueueObject 中保存著信號。這樣做以避免一個線程在調用 queueObject.doWait()之前被另一個調用 unlock()并隨之調用 queueObject.doNotify()的線程重入,從而導致信號丟失。queueObject.doWait()調用放置在 synchronized(this)塊之外,以避免被 monitor 嵌套鎖死,所以另外的線程可以解鎖,只要當沒有線程在 lock 方法的 synchronized(this)塊中執行即可。

最后,注意到 queueObject.doWait()在 try – catch 塊中是怎樣調用的。在 InterruptedException 拋出的情況下,線程得以離開 lock(),并需讓它從隊列中移除。

性能考慮

如果比較 Lock 和 FairLock 類,你會注意到在 FairLock 類中 lock()和 unlock()還有更多需要深入的地方。這些額外的代碼會導致 FairLock 的同步機制實現比 Lock 要稍微慢些。究竟存在多少影響,還依賴于應用在 FairLock 臨界區執行的時長。執行時長越大,FairLock 帶來的負擔影響就越小,當然這也和代碼執行的頻繁度相關。

--------------------------

嵌套管程鎖死

嵌套管程鎖死類似于死鎖, 下面是一個嵌套管程鎖死的場景:

線程 1 獲得 A 對象的鎖。

線程 1 獲得對象 B 的鎖(同時持有對象 A 的鎖)。

線程 1 決定等待另一個線程的信號再繼續。

線程 1 調用 B.wait(),從而釋放了 B 對象上的鎖,但仍然持有對象 A 的鎖。

線程 2 需要同時持有對象 A 和對象 B 的鎖,才能向線程 1 發信號。

線程 2 無法獲得對象 A 上的鎖,因為對象 A 上的鎖當前正被線程 1 持有。

線程 2 一直被阻塞,等待線程 1 釋放對象 A 上的鎖。

線程 1 一直阻塞,等待線程 2 的信號,因此,不會釋放對象 A 上的鎖,

而線程 2 需要對象 A 上的鎖才能給線程 1 發信號……

你可以能會說,這是個空想的場景,好吧,讓我們來看看下面這個比較挫的 Lock 實現:

//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,因為線程不會繼續調用 monitorObject.wait(),那么一切都沒有問題 。但是如果 isLocked 等于 true,調用 lock()方法的線程會在 monitorObject.wait()上阻塞。

這里的問題在于,調用 monitorObject.wait()方法只釋放了 monitorObject 上的管程對象,而與”this“關聯的管程對象并沒有釋放。換句話說,這個剛被阻塞的線程仍然持有”this”上的鎖。

(校對注:如果一個線程持有這種 Lock 的時候另一個線程執行了 lock 操作)當一個已經持有這種 Lock 的線程想調用 unlock(),就會在 unlock()方法進入 synchronized(this)塊時阻塞。這會一直阻塞到在 lock()方法中等待的線程離開 synchronized(this)塊。但是,在 unlock 中 isLocked 變為 false,monitorObject.notify()被執行之后,lock()中等待的線程才會離開 synchronized(this)塊。

簡而言之,在 lock 方法中等待的線程需要其它線程成功調用 unlock 方法來退出 lock 方法,但是,在 lock()方法離開外層同步塊之前,沒有線程能成功執行 unlock()。

結果就是,任何調用 lock 方法或 unlock 方法的線程都會一直阻塞。這就是嵌套管程鎖死。

一個更現實的例子

你可能會說,這么挫的實現方式我怎么可能會做呢?你或許不會在里層的管程對象上調用 wait 或 notify 方法,但完全有可能會在外層的 this 上調。 有很多類似上面例子的情況。例如,如果你準備實現一個公平鎖。你可能希望每個線程在它們各自的 QueueObject 上調用 wait(),這樣就可以每次喚醒一個線程。

下面是一個比較挫的公平鎖實現方式:

//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 方法是怎么調用 queueObject.wait()的,在方法內部有兩個 synchronized 塊,一個鎖定 this,一個嵌在上一個 synchronized 塊內部,它鎖定的是局部變量 queueObject。

當一個線程調用 queueObject.wait()方法的時候,它僅僅釋放的是在 queueObject 對象實例的鎖,并沒有釋放”this”上面的鎖。

現在我們還有一個地方需要特別注意, unlock 方法被聲明成了 synchronized,這就相當于一個 synchronized(this)塊。這就意味著,如果一個線程在 lock()中等待,該線程將持有與 this 關聯的管程對象。所有調用 unlock()的線程將會一直保持阻塞,等待著前面那個已經獲得 this 鎖的線程釋放 this 鎖,但這永遠也發生不了,因為只有某個線程成功地給 lock()中等待的線程發送了信號,this 上的鎖才會釋放,但只有執行 unlock()方法才會發送這個信號。

因此,上面的公平鎖的實現會導致嵌套管程鎖死。更好的公平鎖實現方式可以參考 Starvation and Fairness。

嵌套管程鎖死 VS 死鎖

嵌套管程鎖死與死鎖很像:都是線程最后被一直阻塞著互相等待。

但是兩者又不完全相同。在死鎖中我們已經對死鎖有了個大概的解釋,死鎖通常是因為兩個線程獲取鎖的順序不一致造成的,線程 1 鎖住 A,等待獲取 B,線程 2 已經獲取了 B,再等待獲取 A。如避免死鎖中所說的,死鎖可以通過總是以相同的順序獲取鎖來避免。

但是發生嵌套管程鎖死時鎖獲取的順序是一致的。線程 1 獲得 A 和 B,然后釋放 B,等待線程 2 的信號。線程 2 需要同時獲得 A 和 B,才能向線程 1 發送信號。所以,一個線程在等待喚醒,另一個線程在等待想要的鎖被釋放。

不同點歸納如下:

死鎖中,二個線程都在等待對方釋放鎖。

嵌套管程鎖死中,線程 1 持有鎖 A,同時等待從線程 2 發來的信號,線程 2 需要鎖 A 來發信號給線程 1。

-----------------------------------

Java 中的讀/寫鎖

相比Java 中的鎖(Locks in Java)里 Lock 實現,讀寫鎖更復雜一些。假設你的程序中涉及到對一些共享資源的讀和寫操作,且寫操作沒有讀操作那么頻繁。在沒有寫操作的時候,兩個線程同時讀一個資源沒有任何問題,所以應該允許多個線程能在同時讀取共享資源。但是如果有一個線程想去寫這些共享資源,就不應該再有其它線程對該資源進行讀或寫(譯者注:也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。這就需要一個讀/寫鎖來解決這個問題。

Java5 在 java.util.concurrent 包中已經包含了讀寫鎖。盡管如此,我們還是應該了解其實現背后的原理。

以下是本文的主題

讀/寫鎖的 Java 實現(Read / Write Lock Java Implementation)

讀/寫鎖的重入(Read / Write Lock Reentrance)

讀鎖重入(Read Reentrance)

寫鎖重入(Write Reentrance)

讀鎖升級到寫鎖(Read to Write Reentrance)

寫鎖降級到讀鎖(Write to Read Reentrance)

可重入的 ReadWriteLock 的完整實現(Fully Reentrant ReadWriteLock)

在 finally 中調用 unlock() (Calling unlock() from a finally-clause)

讀/寫鎖的 Java 實現

先讓我們對讀寫訪問資源的條件做個概述:

讀取沒有線程正在做寫操作,且沒有線程在請求寫操作。

寫入沒有線程正在做讀寫操作。

如果某個線程想要讀取資源,只要沒有線程正在對該資源進行寫操作且沒有線程請求對該資源的寫操作即可。我們假設對寫操作的請求比對讀操作的請求更重要,就要提升寫請求的優先級。此外,如果讀操作發生的比較頻繁,我們又沒有提升寫操作的優先級,那么就會產生“饑餓”現象。請求寫操作的線程會一直阻塞,直到所有的讀線程都從 ReadWriteLock 上解鎖了。如果一直保證新線程的讀操作權限,那么等待寫操作的線程就會一直阻塞下去,結果就是發生“饑餓”。因此,只有當沒有線程正在鎖住 ReadWriteLock 進行寫操作,且沒有線程請求該鎖準備執行寫操作時,才能保證讀操作繼續。

當其它線程沒有對共享資源進行讀操作或者寫操作時,某個線程就有可能獲得該共享資源的寫鎖,進而對共享資源進行寫操作。有多少線程請求了寫鎖以及以何種順序請求寫鎖并不重要,除非你想保證寫鎖請求的公平性。

按照上面的敘述,簡單的實現出一個讀/寫鎖,代碼如下

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 類中,讀鎖和寫鎖各有一個獲取鎖和釋放鎖的方法。

讀鎖的實現在 lockRead()中,只要沒有線程擁有寫鎖(writers==0),且沒有線程在請求寫鎖(writeRequests ==0),所有想獲得讀鎖的線程都能成功獲取。

寫鎖的實現在 lockWrite()中,當一個線程想獲得寫鎖的時候,首先會把寫鎖請求數加 1(writeRequests++),然后再去判斷是否能夠真能獲得寫鎖,當沒有線程持有讀鎖(readers==0 ),且沒有線程持有寫鎖(writers==0)時就能獲得寫鎖。有多少線程在請求寫鎖并無關系。

需要注意的是,在兩個釋放鎖的方法(unlockRead,unlockWrite)中,都調用了 notifyAll 方法,而不是 notify。要解釋這個原因,我們可以想象下面一種情形:

如果有線程在等待獲取讀鎖,同時又有線程在等待獲取寫鎖。如果這時其中一個等待讀鎖的線程被 notify 方法喚醒,但因為此時仍有請求寫鎖的線程存在(writeRequests>0),所以被喚醒的線程會再次進入阻塞狀態。然而,等待寫鎖的線程一個也沒被喚醒,就像什么也沒發生過一樣(譯者注:信號丟失現象)。如果用的是 notifyAll 方法,所有的線程都會被喚醒,然后判斷能否獲得其請求的鎖。

用 notifyAll 還有一個好處。如果有多個讀線程在等待讀鎖且沒有線程在等待寫鎖時,調用 unlockWrite()后,所有等待讀鎖的線程都能立馬成功獲取讀鎖 —— 而不是一次只允許一個。

讀/寫鎖的重入

上面實現的讀/寫鎖(ReadWriteLock) 是不可重入的,當一個已經持有寫鎖的線程再次請求寫鎖時,就會被阻塞。原因是已經有一個寫線程了——就是它自己。此外,考慮下面的例子:

Thread 1 獲得了讀鎖。

Thread 2 請求寫鎖,但因為 Thread 1 持有了讀鎖,所以寫鎖請求被阻塞。

Thread 1 再想請求一次讀鎖,但因為 Thread 2 處于請求寫鎖的狀態,所以想再次獲取讀鎖也會被阻塞。 上面這種情形使用前面的 ReadWriteLock 就會被鎖定——一種類似于死鎖的情形。不會再有線程能夠成功獲取讀鎖或寫鎖了。

為了讓 ReadWriteLock 可重入,需要對它做一些改進。下面會分別處理讀鎖的重入和寫鎖的重入。

讀鎖重入

為了讓 ReadWriteLock 的讀鎖可重入,我們要先為讀鎖重入建立規則:

要保證某個線程中的讀鎖可重入,要么滿足獲取讀鎖的條件(沒有寫或寫請求),要么已經持有讀鎖(不管是否有寫請求)。 要確定一個線程是否已經持有讀鎖,可以用一個 map 來存儲已經持有讀鎖的線程以及對應線程獲取讀鎖的次數,當需要判斷某個線程能否獲得讀鎖時,就利用 map 中存儲的數據進行判斷。下面是方法 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;

}

}

代碼中我們可以看到,只有在沒有線程擁有寫鎖的情況下才允許讀鎖的重入。此外,重入的讀鎖比寫鎖優先級高。

寫鎖重入

僅當一個線程已經持有寫鎖,才允許寫鎖重入(再次獲得寫鎖)。下面是方法 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;

}

}

注意在確定當前線程是否能夠獲取寫鎖的時候,是如何處理的。

讀鎖升級到寫鎖

有時,我們希望一個擁有讀鎖的線程,也能獲得寫鎖。想要允許這樣的操作,要求這個線程是唯一一個擁有讀鎖的線程。writeLock()需要做點改動來達到這個目的:

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;

}

}

現在 ReadWriteLock 類就可以從讀鎖升級到寫鎖了。

寫鎖降級到讀鎖

有時擁有寫鎖的線程也希望得到讀鎖。如果一個線程擁有了寫鎖,那么自然其它線程是不可能擁有讀鎖或寫鎖了。所以對于一個擁有寫鎖的線程,再獲得讀鎖,是不會有什么危險的。我們僅僅需要對上面 canGrantReadAccess 方法進行簡單地修改:

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 的完整實現

下面是完整的 ReadWriteLock 實現。為了便于代碼的閱讀與理解,簡單對上面的代碼做了重構。重構后的代碼如下。

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 中調用 unlock()

在利用 ReadWriteLock 來保護臨界區時,如果臨界區可能拋出異常,在 finally 塊中調用 readUnlock()和 writeUnlock()就顯得很重要了。這樣做是為了保證 ReadWriteLock 能被成功解鎖,然后其它線程可以請求到該鎖。這里有個例子:

lock.lockWrite();

try{

//do critical section code, which may throw exception

} finally {

lock.unlockWrite();

}

上面這樣的代碼結構能夠保證臨界區中拋出異常時 ReadWriteLock 也會被釋放。如果 unlockWrite 方法不是在 finally 塊中調用的,當臨界區拋出了異常時,ReadWriteLock 會一直保持在寫鎖定狀態,就會導致所有調用 lockRead()或 lockWrite()的線程一直阻塞。唯一能夠重新解鎖 ReadWriteLock 的因素可能就是 ReadWriteLock 是可重入的,當拋出異常時,這個線程后續還可以成功獲取這把鎖,然后執行臨界區以及再次調用 unlockWrite(),這就會再次釋放 ReadWriteLock。但是如果該線程后續不再獲取這把鎖了呢?所以,在 finally 中調用 unlockWrite 對寫出健壯代碼是很重要的。

-----------------

重入鎖死

重入鎖死與死鎖嵌套管程鎖死非常相似。讀寫鎖兩篇文章中都有涉及到重入鎖死的問題。

當一個線程重新獲取鎖,讀寫鎖或其他不可重入的同步器時,就可能發生重入鎖死。可重入的意思是線程可以重復獲得它已經持有的鎖。Java 的 synchronized 塊是可重入的。因此下面的代碼是沒問題的:

(譯者注:這里提到的鎖都是指的不可重入的鎖實現,并不是 Java 類庫中的 Lock 與 ReadWriteLock 類)

public class Reentrant{

public synchronized outer(){

inner();

}

public synchronized inner(){

//do something

}

}

注意 outer()和 inner()都聲明為 synchronized,這在 Java 中這相當于 synchronized(this)塊(譯者注:這里兩個方法是實例方法,synchronized 的實例方法相當于在 this 上加鎖,如果是 static 方法,則不然,更多閱讀:哪個對象才是鎖?)。如果某個線程調用了 outer(),outer()中的 inner()調用是沒問題的,因為兩個方法都是在同一個管程對象(即 this)上同步的。如果一個線程持有某個管程對象上的鎖,那么它就有權訪問所有在該管程對象上同步的塊。這就叫可重入。若線程已經持有鎖,那么它就可以重復訪問所有使用該鎖的代碼塊。

下面這個鎖的實現是不可重入的:

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();

}

}

如果一個線程在兩次調用 lock()間沒有調用 unlock()方法,那么第二次調用 lock()就會被阻塞,這就出現了重入鎖死。

避免重入鎖死有兩個選擇:

編寫代碼時避免再次獲取已經持有的鎖

使用可重入鎖

至于哪個選擇最適合你的項目,得視具體情況而定。可重入鎖通常沒有不可重入鎖那么好的表現,而且實現起來復雜,但這些情況在你的項目中也許算不上什么問題。無論你的項目用鎖來實現方便還是不用鎖方便,可重入特性都需要根據具體問題具體分析。

-------------------------

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();

}

}

這個版本僅僅是上一個版本的線程安全版本。這一版我們感興趣的是 inc()方法的實現。inc()方法中不再含有一個同步塊。而是被下面這些代碼替代:

boolean updated = false;

while(!updated){

long prevCount = this.count.get();

updated = this.count.compareAndSet(prevCount, prevCount + 1);

}

上面這些代碼并不是一個原子操作。也就是說,對于兩個不同的線程去調用 inc()方法,然后執行 long prevCount = this.count.get()語句,因此獲得了這個計數器的上一個 count。但是,上面的代碼并沒有包含任何的競態條件。

秘密就在于 while 循環里的第二行代碼。compareAndSet()方法調用是一個原子操作。它用一個期望值和 AtomicLong 內部的值去比較,如果這兩個值相等,就把 AtomicLong 內部值替換為一個新值。compareAndSet()通常被 CPU 中的 compare-and-swap 指令直接支持。因此,不需要去同步,也不需要去掛起線程。

假設,這個 AtomicLong 的內部值是 20。然后,兩個線程去讀這個值,都嘗試調用 compareAndSet(20, 20 + 1)。盡管 compareAndSet()是一個原子操作,這個方法也會被這兩個線程相繼執行(某一個時刻只有一個)。

第一個線程會使用期望值 20(這個計數器的上一個值)與 AtomicLong 的內部值進行比較。由于兩個值是相等的,AtomicLong 會更新它的內部值至 21(20 + 1 )。變量 updated 被修改為 true,while 循環結束。

現在,第二個線程調用 compareAndSet(20, 20 + 1)。由于 AtomicLong 的內部值不再是 20,這個調用將不會成功。AtomicLong 的值不會再被修改為 21。變量,updated 被修改為 false,線程將會再次在 while 循環外自旋。這段時間,它會讀到值 21 并企圖把值更新為 22。如果在此期間沒有其它線程調用 inc()。第二次迭代將會成功更新 AtomicLong 的內部值到 22。

為什么稱它為樂觀鎖

上一部分展現的代碼被稱為樂觀鎖(optimistic locking)。樂觀鎖區別于傳統的鎖,有時也被稱為悲觀鎖。傳統的鎖會使用同步塊或其他類型的鎖阻塞對臨界區域的訪問。一個同步塊或鎖可能會導致線程掛起。

樂觀鎖允許所有的線程在不發生阻塞的情況下創建一份共享內存的拷貝。這些線程接下來可能會對它們的拷貝進行修改,并企圖把它們修改后的版本寫回到共享內存中。如果沒有其它線程對共享內存做任何修改, CAS 操作就允許線程將它的變化寫回到共享內存中去。如果,另一個線程已經修改了共享內存,這個線程將不得不再次獲得一個新的拷貝,在新的拷貝上做出修改,并嘗試再次把它們寫回到共享內存中去。

稱之為“樂觀鎖”的原因就是,線程獲得它們想修改的數據的拷貝并做出修改,在樂觀的假在此期間沒有線程對共享內存做出修改的情況下。當這個樂觀假設成立時,這個線程僅僅在無鎖的情況下完成共享內存的更新。當這個假設不成立時,線程所做的工作就會被丟棄,但任然不使用鎖。

樂觀鎖使用于共享內存競用不是非常高的情況。如果共享內存上的內容非常多,僅僅因為更新共享內存失敗,就用浪費大量的 CPU 周期用在拷貝和修改上。但是,如果砸共享內存上有大量的內容,無論如何,你都要把你的代碼設計的產生的爭用更低。

樂觀鎖是非阻塞的

我們這里提到的樂觀鎖機制是非阻塞的。如果一個線程獲得了一份共享內存的拷貝,當嘗試修改時,發生了阻塞,其它線程去訪問這塊內存區域不會發生阻塞。

對于一個傳統的加鎖/解鎖模式,當一個線程持有一個鎖時,其它所有的線程都會一直阻塞直到持有鎖的線程再次釋放掉這個鎖。如果持有鎖的這個線程被阻塞在某處,這個鎖將很長一段時間不能被釋放,甚至可能一直不能被釋放。

非阻塞算法是不容易實現的

正確的設計和實現非阻塞算法是不容易的。在嘗試設計你的非阻塞算法之前,看一看是否已經有人設計了一種非阻塞算法正滿足你的需求。

Java 已經提供了一些非阻塞實現(比如 ConcurrentLinkedQueue),相信在 Java 未來的版本中會帶來更多的非阻塞算法的實現。

除了 Java 內置非阻塞數據結構還有很多開源的非阻塞數據結構可以使用。例如,LAMX Disrupter 和 Cliff Click 實現的非阻塞 HashMap。查看我的Java concurrency references page查看更多的資源。

使用非阻塞算法的好處

非阻塞算法和阻塞算法相比有幾個好處。下面讓我們分別看一下:

選擇

非阻塞算法的第一個好處是,給了線程一個選擇當它們請求的動作不能夠被執行時做些什么。不再是被阻塞在那,請求線程關于做什么有了一個選擇。有時候,一個線程什么也不能做。在這種情況下,它可以選擇阻塞或自我等待,像這樣把 CPU 的使用權讓給其它的任務。不過至少給了請求線程一個選擇的機會。

在一個單個的 CPU 系統可能會掛起一個不能執行請求動作的線程,這樣可以讓其它線程獲得 CPU 的使用權。不過即使在一個單個的 CPU 系統阻塞可能導致死鎖,線程饑餓等并發問題。

沒有死鎖

非阻塞算法的第二個好處是,一個線程的掛起不能導致其它線程掛起。這也意味著不會發生死鎖。兩個線程不能互相彼此等待來獲得被對方持有的鎖。因為線程不會阻塞當它們不能執行它們的請求動作時,它們不能阻塞互相等待。非阻塞算法任然可能產生活鎖(live lock),兩個線程一直請求一些動作,但一直被告知不能夠被執行(因為其他線程的動作)。

沒有線程掛起

掛起和恢復一個線程的代價是昂貴的。沒錯,隨著時間的推移,操作系統和線程庫已經越來越高效,線程掛起和恢復的成本也不斷降低。不過,線程的掛起和戶對任然需要付出很高的代價。

無論什么時候,一個線程阻塞,就會被掛起。因此,引起了線程掛起和恢復過載。由于使用非阻塞算法線程不會被掛起,這種過載就不會發生。這就意味著 CPU 有可能花更多時間在執行實際的業務邏輯上而不是上下文切換。

在一個多個 CPU 的系統上,阻塞算法會對阻塞算法產生重要的影響。運行在 CPUA 上的一個線程阻塞等待運行在 CPU B 上的一個線程。這就降低了程序天生就具備的并行水平。當然,CPU A 可以調度其他線程去運行,但是掛起和激活線程(上下文切換)的代價是昂貴的。需要掛起的線程越少越好。

降低線程延遲

在這里我們提到的延遲指的是一個請求產生到線程實際的執行它之間的時間。因為在非阻塞算法中線程不會被掛起,它們就不需要付昂貴的,緩慢的線程激活成本。這就意味著當一個請求執行時可以得到更快的響應,減少它們的響應延遲。

非阻塞算法通常忙等待直到請求動作可以被執行來降低延遲。當然,在一個非阻塞數據數據結構有著很高的線程爭用的系統中,CPU 可能在它們忙等待期間停止消耗大量的 CPU 周期。這一點需要牢牢記住。非阻塞算法可能不是最好的選擇如果你的數據結構哦有著很高的線程爭用。不過,也常常存在通過重構你的程序來達到更低的線程爭用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容