初識Lock與AbstractQueuedSynchronizer(AQS)

你聽___發表於2018-05-03

初識Lock與AbstractQueuedSynchronizer(AQS)

1. concurrent包的結構層次

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

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介面定義了哪些方法:

void lock(); //獲取鎖 void lockInterruptibly() throws InterruptedException;//獲取鎖的過程能夠響應中斷 boolean tryLock();//非阻塞式響應中斷能立即返回,獲取鎖放回true反之返回fasle boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超時獲取鎖,在超時內或者未中斷的情況下能夠獲取鎖 Condition newCondition();//獲取與lock繫結的等待通知元件,當前執行緒必須獲得了鎖才能進行等待,進行等待時會先釋放鎖,當再次獲取鎖時才能從等待中返回

上面是lock介面下的五個方法,也只是從原始碼中英譯中翻譯了一遍,感興趣的可以自己的去看看。那麼在locks包下有哪些類實現了該介面了?先從最熟悉的ReentrantLock說起。

public class ReentrantLock implements Lock, java.io.Serializable

很顯然ReentrantLock實現了lock介面,接下來我們來仔細研究一下它是怎樣實現的。當你檢視原始碼時你會驚訝的發現ReentrantLock並沒有多少程式碼,另外有一個很明顯的特點是:基本上所有的方法的實現實際上都是呼叫了其靜態記憶體類Sync中的方法,而Sync類繼承了AbstractQueuedSynchronizer(AQS)。可以看出要想理解ReentrantLock關鍵核心在於對佇列同步器AbstractQueuedSynchronizer(簡稱同步器)的理解。

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併發程式設計的藝術》

相關文章