初識Lock與AbstractQueuedSynchronizer(AQS)

1. concurrent包的結構層次

在針對併發編程中,Doug Lea大師爲咱們提供了大量實用,高性能的工具類,針對這些代碼進行研究會讓咱們隊併發編程的掌握更加透徹也會大大提高咱們隊併發編程技術的熱愛。這些代碼在java.util.concurrent包下。以下圖,即爲concurrent包的目錄結構圖。java

concurrent目錄結構.png

其中包含了兩個子包:atomic以及lock,另外在concurrent下的阻塞隊列以及executors,這些就是concurrent包中的精華,以後會一一進行學習。而這些類的實現主要是依賴於volatile以及CAS(關於volatile能夠看這篇文章,關於CAS能夠看這篇文章的3.1節),從總體上來看concurrent包的總體實現圖以下圖所示:編程

concurrent包實現總體示意圖.png

2. lock簡介

咱們下來看concurent包下的lock子包。鎖是用來控制多個線程訪問共享資源的方式,通常來講,一個鎖可以防止多個線程同時訪問共享資源。在Lock接口出現以前,java程序主要是靠synchronized關鍵字實現鎖功能的,而java SE5以後,併發包中增長了lock接口,它提供了與synchronized同樣的鎖功能。**雖然它失去了像synchronize關鍵字隱式加鎖解鎖的便捷性,可是卻擁有了鎖獲取和釋放的可操做性,可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具有的同步特性。**一般使用顯示使用lock的形式以下:設計模式

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

須要注意的是synchronized同步塊執行完成或者遇到異常是鎖會自動釋放,而lock必須調用unlock()方法釋放鎖,所以在finally塊中釋放鎖併發

2.1 Lock接口API

咱們如今就來看看lock接口定義了哪些方法:app

void lock(); //獲取鎖 void lockInterruptibly() throws InterruptedException;//獲取鎖的過程可以響應中斷 boolean tryLock();//非阻塞式響應中斷能當即返回,獲取鎖放回true反之返回fasle boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超時獲取鎖,在超時內或者未中斷的狀況下可以獲取鎖 Condition newCondition();//獲取與lock綁定的等待通知組件,當前線程必須得到了鎖才能進行等待,進行等待時會先釋放鎖,當再次獲取鎖時才能從等待中返回框架

上面是lock接口下的五個方法,也只是從源碼中英譯中翻譯了一遍,感興趣的能夠本身的去看看。那麼在locks包下有哪些類實現了該接口了?先從最熟悉的ReentrantLock提及。ide

public class ReentrantLock implements Lock, java.io.Serializable工具

很顯然ReentrantLock實現了lock接口,接下來咱們來仔細研究一下它是怎樣實現的。當你查看源碼時你會驚訝的發現ReentrantLock並無多少代碼,另外有一個很明顯的特色是:基本上全部的方法的實現實際上都是調用了其靜態內存類Sync中的方法,而Sync類繼承了AbstractQueuedSynchronizer(AQS)。能夠看出要想理解ReentrantLock關鍵核心在於對隊列同步器AbstractQueuedSynchronizer(簡稱同步器)的理解。post

2.2 初識AQS

關於AQS在源碼中有十分具體的解釋:性能

Provides a framework for implementing blocking locks and related
 synchronizers (semaphores, events, etc) that rely on
 first-in-first-out (FIFO) wait queues.  This class is designed to
 be a useful basis for most kinds of synchronizers that rely on a
 single atomic {@code int} value to represent state. Subclasses
 must define the protected methods that change this state, and which
 define what that state means in terms of this object being acquired
 or released.  Given these, the other methods in this class carry
 out all queuing and blocking mechanics. Subclasses can maintain
 other state fields, but only the atomically updated {@code int}
 value manipulated using methods {@link #getState}, {@link
 #setState} and {@link #compareAndSetState} is tracked with respect
 to synchronization.
 
 <p>Subclasses should be defined as non-public internal helper
 classes that are used to implement the synchronization properties
 of their enclosing class.  Class
 {@code AbstractQueuedSynchronizer} does not implement any
 synchronization interface.  Instead it defines methods such as
 {@link #acquireInterruptibly} that can be invoked as
 appropriate by concrete locks and related synchronizers to
 implement their public methods.
複製代碼

同步器是用來構建鎖和其餘同步組件的基礎框架,它的實現主要依賴一個int成員變量來表示同步狀態以及經過一個FIFO隊列構成等待隊列。它的子類必須重寫AQS的幾個protected修飾的用來改變同步狀態的方法,其餘方法主要是實現了排隊和阻塞機制。狀態的更新使用getState,setState以及compareAndSetState這三個方法

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

同步器是實現鎖(也能夠是任意同步組件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。能夠這樣理解兩者的關係:鎖是面向使用者,它定義了使用者與鎖交互的接口,隱藏了實現細節;同步器是面向鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態的管理,線程的排隊,等待和喚醒等底層操做。鎖和同步器很好的隔離了使用者和實現者所需關注的領域。

2.3 AQS的模板方法設計模式

AQS的設計是使用模板方法設計模式,它將一些方法開放給子類進行重寫,而同步器給同步組件所提供模板方法又會從新調用被子類所重寫的方法。舉個例子,AQS中須要重寫的方法tryAcquire:

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

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的方式,在弄懂這點後會lock的實現理解有很大的提高。能夠概括總結爲這麼幾點:

  1. 同步組件(這裏不只僅值鎖,還包括CountDownLatch等)的實現依賴於同步器AQS,在同步組件實現中,使用AQS的方式被推薦定義繼承AQS的靜態內存類;
  2. AQS採用模板方法進行設計,AQS的protected修飾的方法須要由繼承AQS的子類進行重寫實現,當調用AQS的子類的方法時就會調用被重寫的方法;
  3. AQS負責同步狀態的管理,線程的排隊,等待和喚醒這些底層操做,而Lock等同步組件主要專一於實現同步語義;
  4. 在重寫AQS的方式時,使用AQS提供的getState(),setState(),compareAndSetState()方法進行修改同步狀態

AQS可重寫的方法以下圖(摘自《java併發編程的藝術》一書):

AQS可重寫的方法.png

在實現同步組件時AQS提供的模板方法以下圖:

AQS提供的模板方法.png

AQS提供的模板方法能夠分爲3類:

  1. 獨佔式獲取與釋放同步狀態;
  2. 共享式獲取與釋放同步狀態;
  3. 查詢同步隊列中等待線程狀況;

同步組件經過AQS提供的模板方法實現本身的同步語義。

3. 一個例子

下面使用一個例子來進一步理解下AQS的使用。這個例子也是來源於AQS源碼中的example。

class Mutex implements Lock, java.io.Serializable {
    // Our internal helper class
    // 繼承AQS的靜態內存類
    // 重寫方法
    private static class Sync extends AbstractQueuedSynchronizer {
        // Reports whether in locked state
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // Acquires the lock if state is zero
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // Releases the lock by setting state to zero
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // Provides a Condition
        Condition newCondition() {
            return new ConditionObject();
        }

        // Deserializes properly
        private void readObject(ObjectInputStream s)
                throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

    // The sync object does all the hard work. We just forward to it.
    private final Sync sync = new Sync();
    //使用同步器的模板方法實現本身的同步語義
    public void lock() {
        sync.acquire(1);
    }

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}
複製代碼

MutexDemo:

public class MutextDemo {
    private static Mutex mutex = new Mutex();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                mutex.lock();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    mutex.unlock();
                }
            });
            thread.start();
        }
    }
}
複製代碼

執行狀況:

mutex的執行狀況.png

上面的這個例子實現了獨佔鎖的語義,在同一個時刻只容許一個線程佔有鎖。MutexDemo新建了10個線程,分別睡眠3s。從執行狀況也能夠看出來當前Thread-6正在執行佔有鎖而其餘Thread-7,Thread-8等線程處於WAIT狀態。按照推薦的方式,Mutex定義了一個繼承AQS的靜態內部類Sync,而且重寫了AQS的tryAcquire等等方法,而對state的更新也是利用了setState(),getState(),compareAndSetState()這三個方法。在實現實現lock接口中的方法也只是調用了AQS提供的模板方法(由於Sync繼承AQS)。從這個例子就能夠很清楚的看出來,在同步組件的實現上主要是利用了AQS,而AQS「屏蔽」了同步狀態的修改,線程排隊等底層實現,經過AQS的模板方法能夠很方便的給同步組件的實現者進行調用。而針對用戶來講,只須要調用同步組件提供的方法來實現併發編程便可。同時在新建一個同步組件時須要把握的兩個關鍵點是:

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

通俗點說,由於AQS總體設計思路採用模板方法設計模式,同步組件以及AQS的功能實際上別切分紅各自的兩部分:

同步組件實現者的角度:

經過可重寫的方法:獨佔式: tryAcquire()(獨佔式獲取同步狀態),tryRelease()(獨佔式釋放同步狀態);共享式 :tryAcquireShared()(共享式獲取同步狀態),tryReleaseShared()(共享式釋放同步狀態);告訴AQS怎樣判斷當前同步狀態是否成功獲取或者是否成功釋放。同步組件專一於對當前同步狀態的邏輯判斷,從而實現本身的同步語義。這句話比較抽象,舉例來講,上面的Mutex例子中經過tryAcquire方法實現本身的同步語義,在該方法中若是當前同步狀態爲0(即該同步組件沒被任何線程獲取),當前線程能夠獲取同時將狀態更改成1返回true,不然,該組件已經被線程佔用返回false。很顯然,該同步組件只能在同一時刻被線程佔用,Mutex專一於獲取釋放的邏輯來實現本身想要表達的同步語義。

AQS的角度

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

總的來講,同步組件經過重寫AQS的方法實現本身想要表達的同步語義,而AQS只須要同步組件表達的true和false便可,AQS會針對true和false不一樣的狀況作不一樣的處理,至於底層實現,能夠看這篇文章

參考文獻

《java併發編程的藝術》

相關文章
相關標籤/搜索