線程間的同步與通信(4)——Lock 和 Condtion

前言

系列文章目錄

前面幾篇我們學習了synchronized同步代碼塊,瞭解了java的內置鎖,並學習了監視器鎖的wait/notify機制。在大多數情況下,內置鎖都能很好的工作,但它在功能上存在一些侷限性,例如無法實現非阻塞結構的加鎖規則等。爲了拓展同步代碼塊中的監視器鎖,java 1.5 開始,出現了lock接口,它實現了可定時、可輪詢與可中斷的鎖獲取操作,公平隊列,以及非塊結構的鎖。

與內置鎖不同,Lock是一種顯式鎖,它更加“危險”,因爲在程序離開被鎖保護的代碼塊時,不會像監視器鎖那樣自動釋放,需要我們手動釋放鎖。所以,在我們使用lock鎖時,一定要記得:
在finally塊中調用lock.unlock()手動釋放鎖!!!
在finally塊中調用lock.unlock()手動釋放鎖!!!
在finally塊中調用lock.unlock()手動釋放鎖!!!

Lock接口

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

值得注意的是,只有擁有的鎖的線程才能釋放鎖,並且,必須顯式地釋放鎖,這一點和離開同步代碼塊就自動被釋放的監視器鎖是不同的。

newCondition

Lock接口還定義了一個newCondition方法:

Condition newCondition();

該方法將創建一個綁定在當前Lock對象上的Condition對象,這說明Condition對象和Lock對象是對應的,一個Lock對象可以創建多個Condition對象,它們是一個對多的關係。

Condition 接口

上面我們說道,Lock接口中定義了newCondition方法,它返回一個關聯在當前Lock對象上的Condition對象,下面我們來看看這個Condition對象是個啥。

每一個新工具的出現總是爲了解決一定的問題,Condition接口的出現也不例外。
如果說Lock接口的出現是爲了拓展現有的監視器鎖,那麼Condition接口的出現就是爲了拓展同步代碼塊中的wait, notify機制。

監視器鎖的 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種情況之一:

  1. 其他線程調用了notify/signal方法,並且當前線程恰好是被選中來喚醒的那一個
  2. 其他線程調用了notifyAll/signalAll方法
  3. 其他線程中斷了當前線程
  4. 超時時間到了

其中,第三條會拋出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接口具體實現的例子。

(完)

系列文章目錄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章