併發編程之Lock接口與AQS(AbstractQueuedSynchronizer)原理設計

關於併發的jdk源碼結構以下: html

image.png

從總體上來看concurrent包的總體實現圖以下圖所示: java

今天主要來學習下關於lock以及AQS的實現:設計模式

Lock實現提供比使用synchronized方法和語句能夠得到的更普遍的鎖定操做。 它們容許更靈活的結構化,可能具備徹底不一樣的屬性,而且能夠支持多個相關聯的對象Condition 。在Lock接口出現以前,Java程序是靠synchronized關鍵字實現鎖功能的,而Java SE 5以後,併發包中新增了Lock接口(以及相關實現類)用來實現鎖功能,它提供了與synchronized關鍵字相似的同步功能,只是在使用時須要顯式地獲取和釋放鎖。擁有了鎖獲取與釋放的可操做性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具有的同步特性。api

使用方式:安全

Lock lock = new ReentrantLock(); lock.lock();
try {
} finally {
  lock.unlock();
}
複製代碼

使用注意:
1.在finally塊中釋放鎖,目的是保證在獲取到鎖以後,最終可以被釋放。避免鎖泄露。 2.避免在try中加鎖,若是在獲取鎖(自定義鎖的實現)時發生了異常,異常拋出的同時,也會致使鎖無端釋放。 與synchronized區別: bash

而後咱們能夠來看下lock中的幾個api:數據結構

隊列同步器併發

隊列同步器AbstractQueuedSynchronizer(如下簡稱同步器),是用來構建鎖或者其餘同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,經過內置的FIFO隊列來完成資源獲取線程的排隊工做。框架

同步器的主要使用方式是繼承,子類經過繼承同步器並實現它的抽象方法來管理同步狀態在抽象方法的實現過程當中免不了要對同步狀態進行更改,主要使用如下三種方法進行更改:getState()、setState(int newState)和compareAndSetState(int expect,int update))來進行操做,由於它們可以保證狀態的改變是安全的。ide

子類比較推薦被定義爲自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既能夠支持獨佔式地獲取同步狀態,也能夠支持共享式地獲取同步狀態,這樣就能夠方便實現不一樣類型的同步組件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)。

與繼承了Lock接口的不一樣鎖之間的關係:

鎖是面向使用者,它定義了使用者與鎖交互的接口,隱藏了實現細節;同步器是面向鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態的管理,線程的排隊,等待和喚醒等底層操做。鎖和同步器很好的隔離了使用者和實現者所需關注的領域。

在AQS有一個靜態內部類Node,其中有這樣一些屬性:

volatile int waitStatus //節點狀態
volatile Node prev //當前節點/線程的前驅節點
volatile Node next; //當前節點/線程的後繼節點
volatile Thread thread;//加入同步隊列的線程引用
Node nextWaiter;//等待隊列中的下一個節點

節點的狀態有如下這些:
int CANCELLED =  1//節點從同步隊列中取消
int SIGNAL    = -1//後繼節點的線程處於等待狀態,若是當前節點釋放同步狀態會通知後繼節點,使得後繼節點的線程可以運行;
int CONDITION = -2//當前節點進入等待隊列中
int PROPAGATE = -3//表示下一次共享式同步狀態獲取將會無條件傳播下去
int INITIAL = 0;//初始狀態
複製代碼

如今咱們知道了節點的數據結構類型,而且每一個節點擁有其前驅和後繼節點,很顯然這是一個雙向隊列。

AQS的模板方法設計模式

同步器的設計是基於模板方法模式的,也就是說,使用者須要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。 例如: AQS中須要重寫的方法tryAcquire

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}
複製代碼

ReentrantLock

public class ReentrantLock implements Lock, java.io.Serializable {
/** 同步器提供全部實現機制* /私有最終同步同步*/
    private final Sync sync;
  abstract static class Sync extends AbstractQueuedSynchronizer {} 實現該同步器
複製代碼

ReentrantLock中NonfairSync(繼承AQS)會重寫該方法爲:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
複製代碼

而AQS中的模板方法acquire():

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
 }
複製代碼

會調用tryAcquire方法,而此時當繼承AQS的NonfairSync調用模板方法acquire時就會調用已經被NonfairSync重寫的tryAcquire方法。這就是使用AQS的方式.

總結下如下幾點:

1.不一樣的鎖實現主要經過繼承Lock,而且設計一個相關依賴的靜態內部類,sync(實現AQS)做爲實現同步的組件。

2.AQS採用了模板方法進行設計,AQS的protected修飾的方法須要由繼承AQS的子類進行重寫實現,當調用AQS的子類的方法時就會調用被重寫的方法。

3.AQS負責同步狀態的管理,線程的排隊,等待和喚醒這些底層操做

4.在重寫AQS的方式時,使用AQS提供的getState(),setState(),compareAndSetState()方法進行修改同步狀態。

同步器可重寫的方法與描述如表:

實現自定義同步組件時,將會調用同步器提供的模板方法,這些(部分)模板方法與描述以下

列舉一些設計思路:

ReentrantLock

好比可重入鎖(ReentrantLock),根據是否公平調度分爲兩種實現,內部有兩種實現AQS的同步器:這裏忽略重入性等。

static final class NonfairSync extends Sync
static final class FairSync extends Sync

sync: abstract static class Sync extends AbstractQueuedSynchronizer
複製代碼

AQS主要是提供的getState(),setState(),compareAndSetState()方法進行修改同步狀態,根據不一樣鎖的特色,負責同步狀態的管理,線程的排隊,等待和喚醒這些底層操做,來造成不一樣的鎖,具有的特徵。

再好比讀寫鎖,實現了獲取讀鎖時,能夠重寫tryAcquireShared,獲取寫鎖時,仍是重寫tryAcquire。

同步器提供的模板方法基本上分爲3類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步隊列中的等待線程狀況。自定義同步組件將使用同步器提供的模板方法來實現本身的同步語義。

咱們也能夠本身來實現一個:

public class Mutex implements Lock{

    // 靜態內部類,自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer{

        // 是否處於佔用狀態
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        @Override
        protected boolean tryAcquire(int arg) {
            //若是當前狀態值等於預期值,則以原子方式將同步狀態設置爲給定的更新*值。 
            //*此操做具備{@code volatile} read *和write的內存語義。
            if(compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 釋放鎖,將狀態設置爲0
        @Override
        protected boolean tryRelease(int releases) {
            if(getState() == 0){ throw new IllegalMonitorStateException();}
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // 返回一個condition,每一個condition都包含了一個condition實例
        Condition newCondition() { return new ConditionObject(); }
    }

    // 僅須要將操做代理到Sync上便可
    private final Sync sync = new Sync();
    @Override
    public void lock() { sync.acquire(1); }
    @Override
    public boolean tryLock() { return sync.tryAcquire(1); }
    @Override
    public void unlock() { sync.release(1); }
    @Override
    public Condition newCondition() { return sync.newCondition(); }
    public boolean isLocked() { return sync.isHeldExclusively(); }
    public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}
複製代碼

獨佔鎖Mutex是一個自定義同步組件,它在同一時刻只容許一個線程佔有鎖。Mutex中定義了一個靜態內部類,該內部類繼承了同步器並實現了獨佔式獲取和釋放同步狀態。在tryAcquire(int acquires)方法中,若是通過CAS設置成功(同步狀態設置爲1),則表明獲取了同步狀態,而在tryRelease(int releases)方法中只是將同步狀態重置爲0。用戶使用Mutex時並不會直接和內部同步器的實現打交道,而是調用Mutex提供的方法,在Mutex的實現中,以獲取鎖的lock()方法爲例,只須要在方法實現中調用同步器的模板方法acquire(int args)便可,當前線程調用該方法獲取同步狀態失敗後會被加入到同步隊列中等待,這樣就大大下降了實現一個可靠自定義同步組件的門檻。

測試以下:

public static void main(String[] args){
       // CountDownLatch countDownLatch = new CountDownLatch(1);
        Mutex mutex = new Mutex();
      //  countDownLatch.await();
        for(int i=0;i<10;i++){
            new Thread(() -> {
                    mutex.lock();
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName() + Thread.currentThread().getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    mutex.unlock();
                }
                }).start();
        }

複製代碼

結果如圖所示:

按照推薦的方式,Mutex定義了一個繼承AQS的靜態內部類Sync,而且重寫了AQS的tryAcquire等等方法,而對state的更新也是利用了setState(),getState(),compareAndSetState()這三個方法。在實現實現lock接口中的方法也只是調用了AQS提供的模板方法(由於Sync繼承AQS)。

在同步組件的實現上主要是利用了AQS,而AQS「屏蔽」了同步狀態的修改,線程排隊等底層實現,經過AQS的模板方法能夠很方便的給同步組件的實現者進行調用。

總結: 實現同步組件時推薦定義繼承AQS的靜態內存類,並重寫須要的protected修飾的方法; 同步組件語義的實現依賴於AQS的模板方法,而AQS模板方法又依賴於被AQS的子類所重寫的方法。

同步組件實現者的角度: 經過可重寫的方法:獨佔式: tryAcquire()(獨佔式獲取同步狀態),tryRelease()(獨佔式釋放同步狀態);共享式 :tryAcquireShared()(共享式獲取同步狀態),tryReleaseShared()(共享式釋放同步狀態);告訴AQS怎樣判斷當前同步狀態是否成功獲取或者是否成功釋放。

同步組件專一於對當前同步狀態的邏輯判斷,從而實現本身的同步語義。Mutex專一於獲取釋放的邏輯來實現本身想要表達的同步語義。

AQS的角度

而對AQS來講,只須要同步組件返回的true和false便可,由於AQS會對true和false會有不一樣的操做,true會認爲當前線程獲取同步組件成功直接返回,而false的話就AQS也會將當前線程插入同步隊列等一系列的方法。

整理不易,喜歡點個贊

相關文章
相關標籤/搜索