Java併發程式設計之鎖機制之Lock介面

AndyandJennifer發表於2018-10-11

該文章屬於《Java併發程式設計》系列文章,如果想了解更多,請點選《Java併發程式設計之總目錄》

前言

在上篇文章《Java併發程式設計之鎖機制之引導篇》及相關實現類,我們大致瞭解了Lock介面(以及相關實現類)在併發程式設計重要作用。接下來我們就來具體瞭解Lock介面中宣告的方法以及使用優勢。

Lock簡介

Lock 介面實現類提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。此實現允許更靈活的結構,可以具有差別很大的屬性,可以支援多個相關的 Condition (Condition實現類ConditonObject來實現執行緒的通知/與喚醒機制,關於Condition後期會進行介紹)物件。

鎖是用於控制多執行緒訪問共享資源的工具。通常,鎖提供對共享資源的獨佔訪問:一次只有一個執行緒可以獲取鎖,對共享資源的所有訪問都需要首先獲取鎖。但是,一些鎖可以允許同時訪問共享資源,例如ReadWriteLock

雖然使用關鍵字synchronized修飾的方法或程式碼塊,會使得在監視器模式(ObjectMonitor)下程式設計變得非常容易(通過synchronized塊或者方法所提供的隱式獲取釋放鎖的便捷性)。雖然這種方式簡化了鎖的管理,但是某些情況下,還是建議採用Lock介面(及其相關子類)提供的顯示的鎖的獲取和釋放。例如,針對一個場景,手把手進行鎖獲取和釋放,先獲得鎖A,然後再獲取鎖B,當鎖B獲得後,釋放鎖A同時獲取鎖C,當鎖C獲得後,再釋放B同時獲取鎖D,以此類推。這種場景下, synchronized關鍵字就不那麼容易實現了,而Lock介面的實現類允許鎖在不同的作用範圍內獲取和釋放,並允許以任何順序獲取和釋放多個鎖。

Lock介面中的方法

關於Lock介面中涉及到的方法具體如下:(建議直接在PC端檢視,手機上有可能看的不是很清楚)

lock_method.png
從上表中,我們就可以得出使用Lock介面實現的鎖機制與使用傳統的synchronized的區別

  1. 嘗試非阻塞地獲取鎖:當執行緒嘗試獲取鎖,如果這一時刻鎖沒有被其他執行緒獲取到,則成功獲取並持有鎖。
  2. 能被中斷的獲取鎖:與synchronized不同,獲取到鎖的執行緒能夠響應中斷,當獲取到鎖的執行緒被中斷時,中斷異常會被丟擲,同時鎖也會被釋放。
  3. 超時獲取鎖:在指定的截止時間之前獲取鎖,如果截止時間到了任然無法獲取到鎖,則返回。

Lock簡單使用與注意事項

其中Lock的使用方式也很簡單,具體程式碼如下所示:

Lock lock = ....;具體實現類
lock.lock();
try {
} finally {
lock.unlock();//建議在finally中釋放鎖
}
複製程式碼

當鎖定和解鎖發生在不同的範圍時,一定要注意確保在持有鎖時執行的所有程式碼都受到try-finally或try-catch的保護,以確保在必要時釋放鎖。不要將獲取鎖的過程寫在try塊中,因為如果在獲取鎖(自定義鎖的實現)時發生了異常,異常丟擲的同時,也會導致鎖無故釋放(因為一旦發生異常,就會走finally語句,如果這個異常(可能是使用者自定義異常,使用者可以自己處理)需要執行緒1來處理,但是接著執行了lock.unlock()語句導致了鎖的釋放。那麼其他執行緒就可以操作共享資源。有可能破壞程式的執行結果)。

Lock相關實現類實現鎖機制

為了使用Lock介面實現相關鎖功能時,會涉及以下類和介面,這裡還是把上篇文章提到的UML圖展示出來:

lock.png

上圖中,

  1. 綠色部分為:其中ReentrantLock(重入鎖)、WriteLock、ReadLock都是Lock的實現類。Segment為ReentrantLock的子類(在後續文章,ConcurrentHashMap的講解中我們會提及)。 ReentrantReadWriteLock (讀寫鎖)的實現使用了WriteLock與ReadLock類。
  2. 紫色部分為:其中AbstractQueuedSynchronizerAbstractQueuedLongSynchronizer都為AbstractOwnableSynchronizer的子類,該兩個類中都維護了一個同步佇列,用於執行緒的併發執行。在該兩個類中擁有名為ConditionObject(為Conditon的實現類)的內部類,只是其內部實現不同。在ConditionObject內部維護了一個等待佇列,用於控制執行緒的等待與喚醒。

基本程式碼結構

在瞭解了Lock相關實現類實現鎖機制後,這裡給實現該鎖機制的大致程式碼結構(根據不同需求,部分方法實現可能不一樣,這裡只是一個參考,並不是樣本程式碼)。具體程式碼如下所示:

class LockImpl implements Lock {

    private final sync mSync = new sync();
    @Override
    public void lock() {
        mSync.acquire(1);
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        mSync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return mSync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return mSync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        mSync.release(1);
    }

    @Override
    public Condition newCondition() {
        return mSync.newCondition();
    }
    
	 //這裡也可以繼承AbstractQueuedLongSynchronizer
    private static class sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean isHeldExclusively() {...}
        @Override
        protected boolean tryAcquire(int arg) {...}
        @Override
        protected boolean tryRelease(int arg) {...}
        @Override
        protected int tryAcquireShared(int arg) {...}
        @Override
        protected boolean tryReleaseShared(int arg) {...}
        final ConditionObject newCondition() {...}
    }
}
複製程式碼

從程式碼中我們可以看出,在整個Lock介面下實現的鎖機制中,AQS(這裡我們將AbstractQueuedSynchronizer 或AbstractQueuedLongSynchronizer統稱為AQS)是實現鎖的關鍵,整個鎖的實現是在Lock類的實現類中聚合AQS來實現的,從程式碼層面上來說,Lock介面(及其實現類)是面向使用者的,它定義了使用者與鎖互動的介面(比如可以允許兩個執行緒並行訪問),隱藏了實現細節。AQS與Condition才是真正的實現者,它簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。

總結

  1. Lock介面(及其實現類)相比synchronized有如下優點:
  • 鎖的釋放與獲取不在是隱式的,允許鎖在不同的作用範圍內獲取和釋放`,並允許以任何順序獲取和釋放多個鎖。
  • 能被中斷的獲取鎖,獲取到鎖的執行緒能夠響應中斷,當獲取到鎖的執行緒被中斷時,中斷異常會被丟擲,同時鎖也會被釋放
  • 超時獲取鎖:在指定的截止時間之前獲取鎖,如果截止時間到了任然無法獲取到鎖,則返回。
  1. 在使用Lock的時候注意,一定要確保必要時釋放鎖
  2. 在整個Lock介面下實現的鎖機制中,AQS(上文進行了統稱)與Condition才是真正的實現者。

相關文章