Java多線程進階(六)—— J.U.C之locks框架:AQS綜述(1)

241.jpg

本文首發於一世流雲的專欄: https://segmentfault.com/blog...

1、AQS簡介

AbstractQueuedSynchronizer抽象類(如下簡稱AQS)是整個java.util.concurrent包的核心。在JDK1.5時,Doug Lea引入了J.U.C包,該包中的大多數同步器都是基於AQS來構建的。AQS框架提供了一套通用的機制來管理同步狀態(synchronization state)、阻塞/喚醒線程、管理等待隊列。java

咱們所熟知的ReentrantLock、CountDownLatch、CyclicBarrier等同步器,其實都是經過內部類實現了AQS框架暴露的API,以此實現各種同步器功能。這些同步器的主要區別其實就是對同步狀態(synchronization state)的定義不一樣。node

AQS框架,分離了構建同步器時的一系列關注點,它的全部操做都圍繞着資源——同步狀態(synchronization state)來展開,並替用戶解決了以下問題:segmentfault

  1. 資源是能夠被同時訪問?仍是在同一時間只能被一個線程訪問?(共享/獨佔功能)
  2. 訪問資源的線程如何進行併發管理?(等待隊列)
  3. 若是線程等不及資源了,如何從等待隊列退出?(超時/中斷)

這實際上是一種典型的模板方法設計模式:父類(AQS框架)定義好骨架和內部操做細節,具體規則由子類去實現。
AQS框架將剩下的一個問題留給用戶:
什麼是資源?如何定義資源是否能夠被訪問?設計模式

咱們來看下幾個常見的同步器對這一問題的定義:安全

同步器 資源的定義
ReentrantLock 資源表示獨佔鎖。State爲0表示鎖可用;爲1表示被佔用;爲N表示重入的次數
CountDownLatch 資源表示倒數計數器。State爲0表示計數器歸零,全部線程均可以訪問資源;爲N表示計數器未歸零,全部線程都須要阻塞。
Semaphore 資源表示信號量或者令牌。State≤0表示沒有令牌可用,全部線程都須要阻塞;大於0表示由令牌可用,線程每獲取一個令牌,State減1,線程沒釋放一個令牌,State加1。
ReentrantReadWriteLock 資源表示共享的讀鎖和獨佔的寫鎖。state邏輯上被分紅兩個16位的unsigned short,分別記錄讀鎖被多少線程使用和寫鎖被重入的次數。

綜上所述,AQS框架提供瞭如下功能:併發

1.1 提供一套模板框架

因爲併發的存在,須要考慮的狀況很是多,所以可否以一種相對簡單的方法來完成這兩個目標就很是重要,由於對於用戶(AQS框架的使用者來講),不少時候並不關心內部複雜的細節。而AQS其實就是利用模板方法模式來實現這一點,AQS中大多數方法都是final或是private的,也就是說Doug Lea並不但願用戶直接使用這些方法,而是隻覆寫部分模板規定的方法。
AQS經過暴露如下API來讓讓用戶本身解決上面提到的「如何定義資源是否能夠被訪問」的問題:框架

鉤子方法 描述
tryAcquire 排它獲取(資源數)
tryRelease 排它釋放(資源數)
tryAcquireShared 共享獲取(資源數)
tryReleaseShared 共享獲取(資源數)
isHeldExclusively 是否排它狀態

1.2 支持中斷、超時

還記得Lock接口中的那些鎖中斷、限時等待、鎖嘗試的方法嗎?這些方法的實現其實AQS都內置提供了。
使用了AQS框架的同步器,都支持下面的操做:工具

  • 阻塞和非阻塞(例如tryLock)同步;
  • 可選的超時設置,讓調用者能夠放棄等待;
  • 可中斷的阻塞操做。

1.3 支持獨佔模式和共享模式

1.4 支持Condition條件等待

Condition接口,能夠看作是Obejct類的wait()、notify()、notifyAll()方法的替代品,與Lock配合使用。
AQS框架內部經過一個內部類ConditionObject,實現了Condition接口,以此來爲子類提供條件等待的功能。ui

2、AQS方法說明

在本章第一部分講到,AQS利用了模板方法模式,其中大多數方法都是final或是private的,咱們把這類方法稱爲Skeleton Method,也就是說這些方法是AQS框架自身定義好的骨架,子類是不能覆寫的。
下面會按類別簡述一些比較重要的方法,具體實現細節及原理會在本系列後續部分詳細闡述。this

2.1 CAS操做

CAS,即CompareAndSet,在Java中CAS操做的實現都委託給一個名爲UnSafe類,關於Unsafe類,之後會專門詳細介紹該類,目前只要知道,經過該類能夠實現對字段的原子操做。

方法名 修飾符 描述
compareAndSetState protected final CAS修改同步狀態值
compareAndSetHead private final CAS修改等待隊列的頭指針
compareAndSetTail private final CAS修改等待隊列的尾指針
compareAndSetWaitStatus private static final CAS修改結點的等待狀態
compareAndSetNext private static final CAS修改結點的next指針

2.2 等待隊列的核心操做

方法名 修飾符 描述
enq private 入隊操做
addWaiter private 入隊操做
setHead private 設置頭結點
unparkSuccessor private 喚醒後繼結點
doReleaseShared private 釋放共享結點
setHeadAndPropagate private 設置頭結點並傳播喚醒

2.3 資源的獲取操做

方法名 修飾符 描述
cancelAcquire private 取消獲取資源
shouldParkAfterFailedAcquire private static 判斷是否阻塞當前調用線程
acquireQueued final 嘗試獲取資源,獲取失敗嘗試阻塞線程
doAcquireInterruptibly private 獨佔地獲取資源(響應中斷)
doAcquireNanos private 獨佔地獲取資源(限時等待)
doAcquireShared private 共享地獲取資源
doAcquireSharedInterruptibly private 共享地獲取資源(響應中斷)
doAcquireSharedNanos private 共享地獲取資源(限時等待)
方法名 修飾符 描述
acquire public final 獨佔地獲取資源
acquireInterruptibly public final 獨佔地獲取資源(響應中斷)
acquireInterruptibly public final 獨佔地獲取資源(限時等待)
acquireShared public final 共享地獲取資源
acquireSharedInterruptibly public final 共享地獲取資源(響應中斷)
tryAcquireSharedNanos public final 共享地獲取資源(限時等待)

2.4 資源的釋放操做

方法名 修飾符 描述
release public final 釋放獨佔資源
releaseShared public final 釋放共享資源

3、AQS原理簡述

咱們在第一節中講到,AQS框架分離了構建同步器時的一系列關注點,它的全部操做都圍繞着資源——同步狀態(synchronization state)來展開所以,圍繞着資源,衍生出三個基本問題:

  1. 同步狀態(synchronization state)的管理
  2. 阻塞/喚醒線程的操做
  3. 線程等待隊列的管理

3.1 同步狀態

同步狀態的定義
同步狀態,其實就是資源。AQS使用單個int(32位)來保存同步狀態,並暴露出getState、setState以及compareAndSetState操做來讀取和更新這個狀態。

/**
 * 同步狀態.
 */
private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}
/**
 * 以原子的方式更新同步狀態.
 * 利用Unsafe類實現
 */
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

3.2 線程的阻塞/喚醒

在JDK1.5以前,除了內置的監視器機制外,沒有其它方法能夠安全且便捷得阻塞和喚醒當前線程。
JDK1.5之後,java.util.concurrent.locks包提供了LockSupport類來做爲線程阻塞和喚醒的工具。

3.3 等待隊列

等待隊列,是AQS框架的核心,整個框架的關鍵其實就是如何在併發狀態下管理被阻塞的線程。
等待隊列是嚴格的FIFO隊列,是Craig,Landin和Hagersten鎖(CLH鎖)的一種變種,採用雙向鏈表實現,所以也叫CLH隊列。

1. 結點定義
CLH隊列中的結點是對線程的包裝,結點一共有兩種類型:獨佔(EXCLUSIVE)和共享(SHARED)。
每種類型的結點都有一些狀態,其中獨佔結點使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享結點使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)。

結點狀態 描述
CANCELLED 1 取消。表示後驅結點被中斷或超時,須要移出隊列
SIGNAL -1 發信號。表示後驅結點被阻塞了(當前結點在入隊後、阻塞前,應確保將其prev結點類型改成SIGNAL,以便prev結點取消或釋放時將當前結點喚醒。)
CONDITION -2 Condition專用。表示當前結點在Condition隊列中,由於等待某個條件而被阻塞了
PROPAGATE -3 傳播。適用於共享模式(好比連續的讀操做結點能夠依次進入臨界區,設爲PROPAGATE有助於實現這種迭代操做。)
INITIAL 0 默認。新結點會處於這種狀態
AQS使用CLH隊列實現線程的結構管理,而CLH結構正是用前一結點某一屬性表示當前結點的狀態,之因此這種作是由於在雙向鏈表的結構下,這樣更容易實現取消和超時功能。

next指針:用於維護隊列順序,當臨界區的資源被釋放時,頭結點經過next指針找到隊首結點。
prev指針:用於在結點(線程)被取消時,讓當前結點的前驅直接指向當前結點的後驅完成出隊動做。
static final class Node {
    
    // 共享模式結點
    static final Node SHARED = new Node();
    
    // 獨佔模式結點
    static final Node EXCLUSIVE = null;

    static final int CANCELLED =  1;

    static final int SIGNAL    = -1;

    static final int CONDITION = -2;

    static final int PROPAGATE = -3;

    /**
    * INITAL:      0 - 默認,新結點會處於這種狀態。
    * CANCELLED:   1 - 取消,表示後續結點被中斷或超時,須要移出隊列;
    * SIGNAL:      -1- 發信號,表示後續結點被阻塞了;(當前結點在入隊後、阻塞前,應確保將其prev結點類型改成SIGNAL,以便prev結點取消或釋放時將當前結點喚醒。)
    * CONDITION:   -2- Condition專用,表示當前結點在Condition隊列中,由於等待某個條件而被阻塞了;
    * PROPAGATE:   -3- 傳播,適用於共享模式。(好比連續的讀操做結點能夠依次進入臨界區,設爲PROPAGATE有助於實現這種迭代操做。)
    * 
    * waitStatus表示的是後續結點狀態,這是由於AQS中使用CLH隊列實現線程的結構管理,而CLH結構正是用前一結點某一屬性表示當前結點的狀態,這樣更容易實現取消和超時功能。
    */
    volatile int waitStatus;

    // 前驅指針
    volatile Node prev;

    // 後驅指針
    volatile Node next;

    // 結點所包裝的線程
    volatile Thread thread;

    // Condition隊列使用,存儲condition隊列中的後繼節點
    Node nextWaiter;

    Node() {
    }

    Node(Thread thread, Node mode) { 
        this.nextWaiter = mode;
        this.thread = thread;
    }
}

2. 隊列定義
對於CLH隊列,當線程請求資源時,若是請求不到,會將線程包裝成結點,將其掛載在隊列尾部。
CLH隊列的示意圖以下:

①初始狀態,隊列head和tail都指向空

clipboard.png

②首個線程入隊,先建立一個空的頭結點,而後以自旋的方式不斷嘗試插入一個包含當前線程的新結點

clipboard.png
clipboard.png

/**
 * 以自旋的方式不斷嘗試插入結點至隊列尾部
 *
 * @return 當前結點的前驅結點
 */
private Node enq(final Node node) {
    for (; ; ) {
        Node t = tail;
        if (t == null) { // 若是隊列爲空,則建立一個空的head結點
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

4、總結

本章簡要介紹了AQS的思想和原理,讀者能夠參考Doug Lea的論文,進一步瞭解AQS。直接閱讀AQS的源碼比較漫無目的,後續章節,將從ReentrantLock、CountDownLatch的使用入手,講解AQS的獨佔功能和共享功能。

相關文章
相關標籤/搜索