Java併發編程的藝術(八)——初識Lock與AbstractQueuedSynchronizer(AQS)

1. concurrent包的結構層次

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

concurrentç®å½ç»æ.png

其中包含了兩個子包:atomic以及lock,另外在concurrent下的阻塞隊列以及executors,這些就是concurrent包中的精華,之後會一一進行學習。而這些類的實現主要是依賴於volatile以及CAS,從整體上來看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接口定義了哪些方法:

我們現在就來看看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不同的情況做不同的處理,至於底層實現,可以後面的文章。

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