摘要:為了拓展同步代碼塊中的監視器鎖,開始,出現了接口,它實現了可定時可輪詢與可中斷的鎖獲取操作,公平隊列,以及非塊結構的鎖。
前言
系列文章目錄
前面幾篇我們學習了synchronized同步代碼塊,了解了java的內置鎖,并學習了監視器鎖的wait/notify機制。在大多數情況下,內置鎖都能很好的工作,但它在功能上存在一些局限性,例如無法實現非阻塞結構的加鎖規則等。為了拓展同步代碼塊中的監視器鎖,java 1.5 開始,出現了lock接口,它實現了可定時、可輪詢與可中斷的鎖獲取操作,公平隊列,以及非塊結構的鎖。
與內置鎖不同,Lock是一種顯式鎖,它更加“危險”,因為在程序離開被鎖保護的代碼塊時,不會像監視器鎖那樣自動釋放,需要我們手動釋放鎖。所以,在我們使用lock鎖時,一定要記得:
在finally塊中調用lock.unlock()手動釋放鎖!??!
在finally塊中調用lock.unlock()手動釋放鎖?。?!
在finally塊中調用lock.unlock()手動釋放鎖?。?!
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
典型的使用方式:
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }鎖的獲取
Lock接口定義了四種獲取鎖的方式,下面我們一個個來看
lock()
阻塞式獲取,在沒有獲取到鎖時,當前線程將會休眠,不會參與線程調度,直到獲取到鎖為止,獲取鎖的過程中不響應中斷。
lockInterruptibly()
阻塞式獲取,并且可中斷,該方法將在以下兩種情況之一發生的情況下拋出InterruptedException
在調用該方法時,線程的中斷標志位已經被設為true了
在獲取鎖的過程中,線程被中斷了,并且鎖的獲取實現會響應這個中斷
在InterruptedException拋出后,當前線程的中斷標志位將會被清除
tryLock()
非阻塞式獲取,從名字中也可以看出,try就是試一試的意思,無論成功與否,該方法都是立即返回的
相比前面兩種阻塞式獲取的方式,該方法是有返回值的,獲取鎖成功了則返回true,獲取鎖失敗了則返回false
tryLock(long time, TimeUnit unit)
帶超時機制,并且可中斷
如果可以獲取帶鎖,則立即返回true
如果獲取不到鎖,則當前線程將會休眠,不會參與線程調度,直到以下三個條件之一被滿足:
當前線程獲取到了鎖
其它線程中斷了當前線程
設定的超時時間到了
該方法將在以下兩種情況之一發生的情況下拋出InterruptedException
在調用該方法時,線程的中斷標志位已經被設為true了
在獲取鎖的過程中,線程被中斷了,并且鎖的獲取實現會響應這個中斷
在InterruptedException拋出后,當前線程的中斷標志位將會被清除
如果超時時間到了,當前線程還沒有獲得鎖,則會直接返回false(注意,這里并沒有拋出超時異常)
其實,tryLock(long time, TimeUnit unit)更像是阻塞式與非阻塞式的結合體,即在一定條件下(超時時間內,沒有中斷發生)阻塞,不滿足這個條件則立即返回(非阻塞)。
這里把四種鎖的獲取方式總結如下:
相對于鎖的獲取,鎖的釋放的方法就簡單的多,只有一個
void unlock();
值得注意的是,只有擁有的鎖的線程才能釋放鎖,并且,必須顯式地釋放鎖,這一點和離開同步代碼塊就自動被釋放的監視器鎖是不同的。
newConditionLock接口還定義了一個newCondition方法:
Condition newCondition();
該方法將創建一個綁定在當前Lock對象上的Condition對象,這說明Condition對象和Lock對象是對應的,一個Lock對象可以創建多個Condition對象,它們是一個對多的關系。
Condition 接口上面我們說道,Lock接口中定義了newCondition方法,它返回一個關聯在當前Lock對象上的Condition對象,下面我們來看看這個Condition對象是個啥。
每一個新工具的出現總是為了解決一定的問題,Condition接口的出現也不例外。
如果說Lock接口的出現是為了拓展現有的監視器鎖,那么Condition接口的出現就是為了拓展同步代碼塊中的wait, notify機制。
通常情況下,我們調用wait方法,主要是因為一定的條件沒有滿足,我們把需要滿足的事件或條件稱作條件謂詞。
而另一方面,由前面幾篇介紹synchronized原理的文章我們知道,所有調用了wait方法的線程,都會在同一個監視器鎖的wait set中等待,這看上去很合理,但是卻是該機制的短板所在——所有的線程都等待在同一個notify方法上(notify方法指notify()和notifyAll()兩個方法,下同)。每一個調用wait方法的線程可能等待在不同的條件謂詞上,但是有時候即使自己等待的條件并沒有滿足,線程也有可能被“別的線程的”notify方法喚醒,因為大家用的是同一個監視器鎖。這就好比一個班上有幾個重名的同學(使用相同的監視器鎖),老師喊了這個名字(notify方法),結果這幾個同學全都站起來了(等待在監視器鎖上的線程都被喚醒了)。
這樣以來,即使自己被喚醒后,搶到了監視器鎖,發現其實條件還是不滿足,還是得調用wait方法掛起,就導致了很多無意義的時間和CPU資源的浪費。
這一切的根源就在于我們在調用wait方法時沒有辦法來指明究竟是在等待什么樣的條件謂詞上,因此喚醒時,也不知道該喚醒誰,只能把所有的線程都喚醒了。
因此,最好的方式是,我們在掛起時就指明了在什么樣的條件謂詞上掛起,同時,在等待的事件發生后,只喚醒等待在這個事件上的線程,而實現了這個思路的就是Condition接口。
有了Condition接口,我們就可以在同一個鎖上創建不同的喚醒條件,從而在一定條件謂詞滿足后,有針對性的喚醒特定的線程,而不是一股腦的將所有等待的線程都喚醒。
Condition的 await/signal 機制既然前面說了Condition接口的出現是為了拓展現有的wait/notify機制,那我們就先來看看現有的wait/notify機制有哪些方法:
public class Object { public final void wait() throws InterruptedException { wait(0); } public final native void wait(long timeout) throws InterruptedException; public final void wait(long timeout, int nanos) throws InterruptedException { // 這里省略方法的實現 } public final native void notify(); public final native void notifyAll(); }
接下來我們再看看Condition接口有哪些方法:
public interface Condition { void await() throws InterruptedException; long awaitNanos(long nanosTimeout) throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; void awaitUninterruptibly(); boolean awaitUntil(Date deadline) throws InterruptedException; void signal(); void signalAll(); }
對比發現,這里存在明顯的對應關系:
Object 方法 | Condition 方法 | 區別 |
---|---|---|
void wait() | void await() | |
void wait(long timeout) | long awaitNanos(long nanosTimeout) | 時間單位,返回值 |
void wait(long timeout, int nanos) | boolean await(long time, TimeUnit unit) | 時間單位,參數類型,返回值 |
void notify() | void signal() | |
void notifyAll() | void signalAll() | |
- | void awaitUninterruptibly() | Condition獨有 |
- | boolean awaitUntil(Date deadline) | Condition獨有 |
它們在接口的規范上都是差不多的,只不過wait/notify機制針對的是所有在監視器鎖的wait set中的線程,而await/signal機制針對的是所有等待在該Condition上的線程。
這里多說一句,在接口的規范中,wait(long timeout)的時間單位是毫秒(milliseconds), 而awaitNanos(long nanosTimeout)的時間單位是納秒(nanoseconds), 就這一點而言,awaitNanos這個方法名其實語義上更清晰,并且相對于wait(long timeout, int nanos)這個略顯雞肋的方法(之前的分析中我們已經吐槽過這個方法的實現了),await(long time, TimeUnit unit)這個方法就顯得更加直觀和有效。
另外一點值得注意的是,awaitNanos(long nanosTimeout)是有返回值的,它返回了剩余等待的時間;await(long time, TimeUnit unit)也是有返回值的,如果該方法是因為超時時間到了而返回的,則該方法返回false, 否則返回true。
大家有沒有覺的奇怪,同樣是帶超時時間的等待,為什么wait方式沒有返回值,await方式有返回值呢。
存在即合理,既然多加了返回值,自然是有它的用意,那么這個多加的返回值有什么用呢?
我們知道,當一個線程從帶有超時時間的wait/await方法返回時,必然是發生了以下4種情況之一:
其他線程調用了notify/signal方法,并且當前線程恰好是被選中來喚醒的那一個
其他線程調用了notifyAll/signalAll方法
其他線程中斷了當前線程
超時時間到了
其中,第三條會拋出InterruptedException,是比較容易分辨的;除去這個,當wait方法返回后,我們其實無法區分它是因為超時時間到了返回了,還是被notify返回的。但是對于await方法,因為它是有返回值的,我們就能夠通過返回值來區分:
如果awaitNanos(long nanosTimeout)的返回值大于0,說明超時時間還沒到,則該返回是由signal行為導致的
如果await(long time, TimeUnit unit)返回true, 說明超時時間還沒到,則該返回是由signal行為導致的
源碼的注釋也說了,await(long time, TimeUnit unit)相當于調用awaitNanos(unit.toNanos(time)) > 0
所以,它們的返回值能夠幫助我們弄清楚方法返回的原因。
Condition接口中還有兩個在Object中找不到對應的方法:
void awaitUninterruptibly(); boolean awaitUntil(Date deadline) throws InterruptedException;
前面說的所有的wait/await方法,它們方法的簽名中都拋出了InterruptedException,說明他們在等待的過程中都是響應中斷的,awaitUninterruptibly方法從名字中就可以看出,它在等待鎖的過程中是不響應中斷的,所以沒有InterruptedException拋出。也就是說,它會一直阻塞,直到signal/signalAll被調用。如果在這過程中線程被中斷了,它并不響應這個中斷,只是在該方法返回的時候,該線程的中斷標志位將是true, 調用者可以檢測這個中斷標志位以輔助判斷在等待過程中是否發生了中斷,以此決定要不要做額外的處理。
boolean awaitUntil(Date deadline)和boolean await(long time, TimeUnit unit) 其實作用是差不多的,返回值代表的含義也一樣,只不過一個是相對時間,一個是絕對時間,awaitUntil方法的參數是Date,表示了一個絕對的時間,即截止日期,在這個日期之前,該方法會一直等待,除非被signal或者被中斷。
至此,Lock接口和Condition接口我們就分析完了。
我們將在下一篇中給出Lock接口的具體實現的例子,在逐行分析AQS源碼(4)——Condition接口實現中給出Condition接口具體實現的例子。
(完)
系列文章目錄
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77184.html
摘要:為了避免一篇文章的篇幅過長,于是一些比較大的主題就都分成幾篇來講了,這篇文章是筆者所有文章的目錄,將會持續更新,以給大家一個查看系列文章的入口。 前言 大家好,筆者是今年才開始寫博客的,寫作的初衷主要是想記錄和分享自己的學習經歷。因為寫作的時候發現,為了弄懂一個知識,不得不先去了解另外一些知識,這樣以來,為了說明一個問題,就要把一系列知識都了解一遍,寫出來的文章就特別長。 為了避免一篇...
摘要:前言本篇文章是基于線程間的同步與通信和這篇文章寫的,在那篇文章中,我們分析了接口所定義的方法,本篇我們就來看看對于接口的這些接口方法的具體實現。因此,條件隊列在出隊時,線程并不持有鎖。 前言 本篇文章是基于線程間的同步與通信(4)——Lock 和 Condtion 這篇文章寫的,在那篇文章中,我們分析了Condition接口所定義的方法,本篇我們就來看看AQS對于Condition接口...
摘要:例如,線程需要互相等待,保證所有線程都執行完了之后才能一起通過。獲取正在等待中的線程數注意,這里加了鎖,因為方法可能會被多個線程同時修改。只要有一行沒有處理完,所有的線程都會在處等待,最后一個執行完的線程將會負責喚醒所有等待的線程 前言 系列文章目錄 上一篇 我們學習了基于AQS共享鎖實現的CountDownLatch,本篇我們來看看另一個和它比較像的并發工具CyclicBarrier...
摘要:由此可見,自旋鎖和各有優劣,他們分別適用于競爭不多和競爭激烈的場景中。每一個試圖進入同步代碼塊的線程都會被封裝成對象,它們或在對象的中,或在中,等待成為對象的成為的對象即獲取了監視器鎖。 前言 系列文章目錄 前面兩篇文章我們介紹了synchronized同步代碼塊以及wait和notify機制,大致知道了這些關鍵字和方法是干什么的,以及怎么用。 但是,知其然,并不知其所以然。 例如...
閱讀 909·2021-09-09 09:32
閱讀 2848·2021-09-02 10:20
閱讀 2684·2021-07-23 11:24
閱讀 824·2019-08-30 15:54
閱讀 3630·2019-08-30 15:54
閱讀 1346·2019-08-30 11:02
閱讀 2844·2019-08-26 17:40
閱讀 1122·2019-08-26 13:55