併發——抽象隊列同步器AQS的實現原理

1、前言

  這段時間在研究Java併發相關的內容,一段時間下來算是小有收穫了。ReentrantLockJava併發中的重要部分,因此也是個人首要研究對象,在學習它的過程當中,我發現它是基於抽象隊列同步器AQS實現的,因此我花了點時間學習了一下AQS的實現原理。這篇博客就來講一說AQS的做用,以及它是如何實現的。html


2、正文

2.1 什麼是AQS

  AQS全稱抽象隊列同步器(AbstractQuenedSynchronizer),它是一個能夠用來實現線程同步的基礎框架。固然,它不是咱們理解的Spring這種框架,它是一個類,類名就是AbstractQuenedSynchronizer,若是咱們想要實現一個可以完成線程同步的鎖或者相似的同步組件,就能夠在使用AQS來實現,由於它封裝了線程同步的方式,咱們在本身的類中使用它,就能夠很方便的實現一個咱們本身的鎖。java


2.2 如何使用AQS

  AQS封裝了不少方法,如獲取獨佔鎖,釋放獨佔鎖,獲取共享鎖,釋放共享鎖......咱們能夠經過在本身的實現的同步組件中調用AQS的這些方法來實現一個線程同步的功能。可是,根據AQS的名稱也可以想到,咱們不能直接建立AQS的對象,調用這些方法,由於AQS是一個抽象類,咱們須要繼承AQS,建立它的子類對象來使用它。在實際使用中,通常是在咱們本身的類中,之內部類的方式繼承AQS,而後在內部建立一個對象,在這個類內部使用,好比ReentrantLock中就是定義了一個抽象內部類Sync,繼承AQS,而後定義了一個NonfairSync類,繼承SyncNonfairSync是一個非公平鎖;同時又定義了一個FairSync類繼承SyncFairSync是一個公平鎖node

公平鎖:多個線程按照申請鎖的順序去得到鎖,後申請鎖的線程須要排隊,等它以前的線程得到鎖並釋放後,它才能得到鎖;算法

非公平鎖:線程得到鎖的順序於申請鎖的順序無關,申請鎖的線程能夠直接嘗試得到鎖,誰搶到就是誰的;編程

  咱們繼承了AQS,就能夠直接調用它的方法了嗎?固然不是。Java中提供的抽象組件,都是幫咱們寫好了通用的部分,可是一些具體的部分,還須要咱們本身實現。舉個比較簡單的例子,Java中對自定義類型數組的排序,能夠直接調用工具類的sort方法,sort方法已經實現了排序的算法,可是其中的比較過程是抽象的,須要咱們本身實現,因此咱們通常須要提供一個比較器(Comparator),或者讓自定義類實現Comparable接口。這就是模板方法設計模式。設計模式

模板方法:在一個方法中實現了一個算法的流程,可是其中的一些步驟是抽象的,須要在子類中實現,或者具體使用時實現。模板方法能夠提升算法的複用性,提供了算法的彈性,對於不一樣的需求,能夠通用同一份代碼。數組

  而AQS的實現就是封裝了一系列的模板方法,包括獲取鎖、釋放鎖等,這些都是模板方法。這些方法中調用的一些方法並無具體實現,須要使用者根據本身的需求,在子類中進行實現。下面咱們就來看看AQS中的這些方法。安全


2.3 AQS中的方法

  AQS底層維護一個int類型的變量state來表示當前的同步狀態,根據當前state的值,來判斷當前釋放處於鎖定狀態,或者是其餘狀態。而state的每個值具體是什麼含義,是由咱們本身實現的。咱們繼承AQS時,根據本身的需求,實現一些方法,其中就是經過修改state的值來維持同步狀態。而關於state,主要有如下三個方法:併發

  • **int getState() **:獲取當前同步狀態state的值;
  • **void setState(int newState) **:設置當前同步狀態state的值;
  • **boolean compareAndSetState(int expect, int update) **:使用CAS設置當前同步狀態的值,方法可以保證設置同步狀態時的原子性;參數expectstate的預期舊值,而update是須要修改的新值,若設置成功,方法返回true,不然false

CAS是一種樂觀鎖,若不瞭解,能夠看看這篇博客:併發——詳細介紹CAS機制框架

  接下來咱們再看一看在繼承AQS時,咱們能夠重寫的方法:

  以上這些方法將會在AQS的模板方法中被調用,咱們根據本身的需求,重寫上述方法,控制同步狀態state的值,便可控制線程同步的方式。下面再來看看AQS提供的模板方法:

  AQS提供的模板方法主要分爲三類:

  • 獨佔式地獲取和釋放鎖;
  • 共享式地獲取和釋放鎖;
  • 查詢AQS的同步隊列中正在等待的線程狀況;

  下面咱們就來具體說一說AQS是如何實現線程同步的。


2.4 AQS如何實現線程同步

  前面提過,AQS經過一個int類型的變量state來記錄當前的同步狀態,也能夠理解爲鎖的狀態,根據state的值的不一樣,能夠判斷當前鎖是否已經被獲取。就拿獨佔鎖來講,若咱們要實現的是一個獨佔鎖,則鎖被獲取後,其餘線程將沒法獲取鎖,須要進入阻塞狀態,等待鎖被釋放。而線程獲取鎖就是經過修改state的值來實現的,一個線程修改state成功,則表示它成功得到了鎖;若失敗,則表示已經有其餘線程得到了鎖,則它須要進入阻塞狀態。下面咱們就來聊一聊AQS如何實現維持多個線程等待的。

  首先說明結論:AQS經過一個同步隊列來維護當前獲取鎖失敗,進入阻塞狀態的線程。這個同步隊列是一個雙向鏈表,獲取鎖失敗的線程會被封裝成一個鏈表節點,加入鏈表的尾部排隊,而AQS保存了鏈表的頭節點的引用head以及鏈表的尾節點引用tail。這個同步隊列以下所示:

  在這個同步隊列中,每一個節點對應一個線程,每一個節點都有一個next指針指向它的下一個節點,以及一個prev指針指向它的上一個節點。隊列中的頭節點head就是當前已經獲取了鎖,正在執行的線程對應的節點;而以後的這些節點,則對應着獲取鎖失敗,正在排隊的線程。

  當一個線程獲取鎖失敗,它會被封裝成一個Node,加入同步隊列的尾部排隊,同時線程會進入阻塞狀態。也就是說,在同步隊列中,除了頭節點對應的線程是運行狀態,其他的線程都是等待睡眠狀態。而當頭節點對應的線程釋放鎖時,它會喚醒它的下一個節點(也就是上圖中的第二個節點),被喚醒的節點對應的線程開始嘗試獲取鎖,若獲取成功,它就會將本身置爲head,而後將原來的head移出隊列。接下來咱們就經過源碼,具體分析一下AQS的實現過程。


2.5 獨佔鎖的獲取與釋放過程

(1)獲取鎖的實現

  AQS的鎖功能齊全,它既能夠用來實現獨佔鎖,也能夠用來實現共享鎖。

獨佔鎖:也叫排他鎖,即鎖只能由一個線程獲取,若一個線程獲取了鎖,則其餘想要獲取鎖的線程只能等待,直到鎖被釋放。好比說寫鎖,對於寫操做,每次只能由一個線程進行,若多個線程同時進行寫操做,將極可能出現線程安全問題;

共享鎖:鎖能夠由多個線程同時獲取,鎖被獲取一次,則鎖的計數器+1。比較典型的就是讀鎖,讀操做並不會產生反作用,因此能夠容許多個線程同時對數據進行讀操做,而不會有線程安全問題,固然,前提是這個過程當中沒有線程在進行寫操做;

  咱們首先分析一下獨佔鎖。在AQS中,經過方法acquire來獲取獨佔鎖,acquire方法的代碼以下:

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

  上面的方法執行流程以下:

  1. 首先調用tryAcquire嘗試獲取一次鎖,若返回true,表示獲取成功,則acquire方法將直接返回;若返回false,則會繼續向後執行acquireQueued方法;

  2. tryAcquire返回false後,將執行acquireQueued,可是這個方法傳入的參數調用了addWaiter方法;

  3. addWaiter方法的做用是將當前線封裝成同步隊列的節點,而後加入到同步隊列的尾部進行排隊,並返回此節點;

  4. addWaiter方法執行完成後,將它的返回值做爲參數,調用acquireQueued方法。acquireQueued方法的做用是讓當前線程在同步隊列中阻塞,而後在被其餘線程喚醒時去獲取鎖;

  5. 若線程被喚醒併成功獲取鎖後,將從acquireQueued方法中退出,同時返回一個boolean值表示當前線程是否被中斷,若被中斷,則會執行下面的selfInterrupt方法,響應中斷;

      下面咱們就來具體分析這個方法中調用的幾個方法的執行流程。首先第一個tryAcquire方法:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

  能夠看到,這個方法的實現僅僅只是拋出了一個異常。咱們以前提過,AQS是基於模板方法設計模式實現的,在其中定義了許多模板方法,在模板方法中會調用一些沒有實現的方法,這些方法須要使用者根據本身的需求實現。而acquire方法就是一個模板方法,其中調用的tryAcquire方法就是須要咱們本身實現的方法。tryAcquire的做用就是嘗試修改state值,也就是獲取鎖,若修改爲功,則返回true,不然返回false。它的實現須要根據AQS的子類具體分析,好比ReentrantLock中的Sync,這裏我就不詳細敘述了,後面寫一篇專門講ReentrantLock的博客。下面來看看addWaiter的源碼:

// 將線程封裝成一個節點,放入同步隊列的尾部
private Node addWaiter(Node mode) {
    // 當前線程封裝成同步隊列的一個節點Node
    Node node = new Node(Thread.currentThread(), mode);
    // 這個節點須要插入到原尾節點的後面,因此咱們在這裏先記下原來的尾節點
    Node pred = tail;
    // 判斷尾節點是否爲空,若爲空表示隊列中尚未節點,則不執行如下步驟
    if (pred != null) {
        // 記錄新節點的前一個節點爲原尾節點
        node.prev = pred;
        // 將新節點設置爲新尾節點,使用CAS操做保證了原子性
        if (compareAndSetTail(pred, node)) {
            // 若設置成功,則讓原來的尾節點的next指向新尾節點
            pred.next = node;
            return node;
        }
    }
    // 若以上操做失敗,則調用enq方法繼續嘗試(enq方法見下面)
    enq(node);
    return node;
}

private Node enq(final Node node) {
    // 使用死循環不斷嘗試
    for (;;) {
        // 記錄原尾節點
        Node t = tail;
        // 若原尾節點爲空,則必須先初始化同步隊列,初始化以後,下一次循環會將新節點加入隊列
        if (t == null) { 
            // 使用CAS設置建立一個默認的節點做爲首屆點
            if (compareAndSetHead(new Node()))
                // 首尾指向同一個節點
                tail = head;
        } else {
            // 如下操做與addWaiter方法中的if語句塊內一致
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

  以上就是addWaiter方法的實現過程,我在代碼中使用註釋對每一步進行了詳細的解析,它的執行過程大體能夠總結爲:將新線程封裝成一個節點,加入到同步隊列的尾部,若同步隊列爲空,則先在其中加入一個默認的節點,再進行加入;若加入失敗,則使用死循環(也叫自旋)不斷嘗試,直到成功爲止。這個過程當中使用CAS保證了添加節點的原子性。下面看看acquireQueued方法的源碼:

/**
 * 讓線程不間斷地獲取鎖,若線程對應的節點不是頭節點的下一個節點,則會進入等待狀態
 * @param node the node
 */
final boolean acquireQueued(final Node node, int arg) {
    // 記錄失敗標誌
    boolean failed = true;
    try {
        // 記錄中斷標誌,初始爲true
        boolean interrupted = false;
        // 循環執行,由於線程在被喚醒後,可能再次獲取鎖失敗,須要重寫進入等待
        for (;;) {
            // 獲取當前線程節點的前一個節點
            final Node p = node.predecessor();
            // 若前一個節點是頭節點,則tryAcquire嘗試獲取鎖,若獲取成功,則執行if中的代碼
            if (p == head && tryAcquire(arg)) {
                // 將當前節點設置爲頭節點
                setHead(node);
                // 將原來的頭節點移出同步隊列
                p.next = null; // help GC
                // 失敗標誌置爲false
                failed = false;
                // 返回中斷標誌,acquire方法能夠根據返回的中斷標誌,判斷當前線程是否被中斷
                return interrupted;
            }
            // shouldParkAfterFailedAcquire方法判斷當前線程是否可以進入等待狀態,
            // 若當前線程的節點不是頭節點的下一個節點,則須要進入等待狀態,
            // 在此方法內部,當前線程會找到它的前驅節點中,第一個還在正常等待或執行的節點,
            // 讓其做爲本身的直接前驅,而後在須要時將本身喚醒(由於其中有些線程可能被中斷),
            // 若找到,則返回true,表示本身能夠進入等待狀態了;
            // 則繼續調用parkAndCheckInterrupt方法,當前線程在這個方法中等待,
            // 直到被其餘線程喚醒,或者被中斷後返回,返回時將返回一個boolean值,
            // 表示這個線程是否被中斷,若爲true,則將執行下面一行代碼,將中斷標誌置爲true
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 上面代碼中只有一個return語句,且return的前一句就是failed = false;
        // 因此只有當異常發生時,failed纔會保持true的狀態運行到此處;
        // 異常多是線程被中斷,也多是其餘方法中的異常,
        // 好比咱們本身實現的tryAcquire方法
        // 此時將取消線程獲取鎖的動做,將它從同步隊列中移除
        if (failed)
            cancelAcquire(node);
    }
}

  以上就是acquireQueued方法的源碼分析。這個方法的做用能夠歸納爲:讓線程在同步隊列中阻塞,直到它成爲頭節點的下一個節點,被頭節點對應的線程喚醒,而後開始獲取鎖,若獲取成功纔會從方法中返回。這個方法會返回一個boolean值,表示這個正在同步隊列中的線程是否被中斷。

  到此,獲取獨佔鎖的實現就分析完畢了。須要注意的是,這些過程當中使用的compareAndSetXXX這種形式的方法,都是基於CAS機制實現的,保證了這些操做的原子性。


(2)釋放鎖的實現

  分析完獲取獨佔鎖的代碼後,咱們再來看看釋放鎖的實現。釋放獨佔鎖是經過release方法實現的:

public final boolean release(int arg) {
    // 調用tryRelease嘗試修改state釋放鎖,若成功,將返回true,不然false
    if (tryRelease(arg)) {
        // 若修改state成功,則表示釋放鎖成功,須要將當前線程移出同步隊列
        // 當前線程在同步隊列中的節點就是head,因此此處記錄head
        Node h = head;
        // 若head不是null,且waitStatus不爲0,表示它是一個裝有線程的正常節點,
        // 在以前提到的addWaiter方法中,若同步隊列爲空,則會建立一個默認的節點放入head
        // 這個默認的節點不包含線程,它的waitStatus就是0,因此不能釋放鎖
        if (h != null && h.waitStatus != 0)
            // 若head是一個正常的節點,則調用unparkSuccessor喚醒它的下一個節點所對應的線程
            unparkSuccessor(h);
        // 釋放成功
        return true;
    }
    // 釋放鎖失敗
    return false;
}

  以上就是同步隊列中頭節點對應的線程釋放鎖的過程。release也是一個模板方法,其中經過調用tryRelease嘗試釋放鎖,而tryRelease也須要使用者本身實現。在以前也說過,頭節點釋放鎖時,須要喚醒它的下一個節點對應的線程,讓這個線程再也不等待,去獲取鎖,而這個過程就是經過unparkSuccessor方法實現的。


2.6 共享鎖的獲取與釋放過程

  前面提到過,AQS不只僅能夠用來實現獨佔鎖,還能夠用來實現共享鎖,下面咱們就來看看AQS中,有關共享鎖的模板方法的實現。首先是獲取共享鎖的實現,在AQS中,定義了acquireShared方法用來獲取共享鎖:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

  能夠看到,這個方法比較簡短。首先調用tryAcquireShared方法嘗試獲取一次共享鎖,即修改state的值,若返回值>=0,則表示獲取成功,線程不受影響,繼續向下執行;若返回值小於0,表示獲取共享鎖失敗,則線程須要進入到同步隊列中等待,調用doAcquireShared方法。acquireShared方法也是AQS的一個模板方法,而其中的tryAcquireShared方法就是須要使用者本身實現的方法。下面咱們來看看doAcquireShared方法的實現:

/**
 * 不間斷地獲取共享鎖,若線程對應的節點不是頭節點的下一個節點,將進入等待狀態
 * 實現與acquireQueued很是相似
 * @param arg the acquire argument
 */
private void doAcquireShared(int arg) {
    // 往同步隊列的尾部添加一個默認節點,Node.SHARED是一個Node常量,
    // 它的值就是一個不帶任何參數的Node對象,也就是new Node();
    final Node node = addWaiter(Node.SHARED);
    // 失敗標誌,默認爲true
    boolean failed = true;
    try {
        // 中斷標誌,用來判斷線程在等待的過程當中釋放被中斷
        boolean interrupted = false;
        // 死循環不斷嘗試獲取共享鎖
        for (;;) {
            // 獲取默認節點的前一個節點
            final Node p = node.predecessor();
            // 判斷當前節點的前一個節點是否爲head節點
            if (p == head) {
                // 嘗試獲取共享鎖
                int r = tryAcquireShared(arg);
                // 若r>0,表示獲取成功
                if (r >= 0) {
                    // 當前線程獲取鎖成功後,調用setHeadAndPropagate方法將當前線程設置爲head
                    // 同時,若共享鎖還能被其餘線程獲取,則在這個方法中也會向後傳遞,喚醒後面的線程
                    setHeadAndPropagate(node, r);
                    // 將原來的head的next置爲null
                    p.next = null; // help GC
                    // 判斷當前線程是否中斷,若被中斷,則調用selfInterrupt方法響應中斷
                    if (interrupted)
                        selfInterrupt();
                    // 失敗標誌置爲false
                    failed = false;
                    return;
                }
            }
            // 如下代碼和獲取獨佔鎖的acquireQueued方法相同,即讓當前線程進入等待狀態
            // 具體解析能夠看上面acquireQueued方法的解析
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

  doAcquireShared方法的實現和獲取獨佔鎖中的acquireQueued方法很相似,可是主要有一點不一樣,那就是線程在被喚醒後,若成功獲取到了共享鎖,還須要判斷共享鎖是否還能被其餘線程獲取,若能夠,則繼續向後喚醒它的下一個節點對應的線程。下面再看看釋放共享鎖的代碼,釋放共享鎖時經過方法releaseShared

public final boolean releaseShared(int arg) {
    // 嘗試修改state的值釋放鎖
    if (tryReleaseShared(arg)) {
        // 若成功,則調用如下方法喚醒後繼節點中的線程
        doReleaseShared();
        return true;
    }
    return false;
}

  releaseShared也是一個模板方法,它經過調用使用者本身實現的tryReleaseShared方法嘗試釋放鎖,修改state的值,若返回true,表示修改爲功,則繼續向下調用doReleaseShared喚醒head的下一個節點對應的線程,讓它開始嘗試獲取鎖;若修改state失敗,則返回false


2.7 使用AQS實現一個鎖

  介紹完上面的內容,下面咱們就來基於AQS實現一個本身的同步器,或者說鎖。咱們須要實現的鎖要求以下:

  實現一個鎖,它是一個共享鎖,可是每次至多支持兩個線程同時獲取鎖,若當前已經有兩個線程獲取了鎖,則其餘獲取鎖的線程須要等待。

  實現代碼以下:

/**
 * 抽象隊列同步器(AQS)使用:
 *      實現一個同一時刻至多隻支持兩個線程同時執行的同步器
 */

// 讓當前類繼承Lock接口
public class TwinLock implements Lock {

    // 定義鎖容許的最大線程數
    private final static int DEFAULT_SYNC_COUNT = 2;
    // 建立一個鎖對象,用以進行線程同步,Sync繼承自AQS
    private final Sync sync = new Sync(DEFAULT_SYNC_COUNT);

    // 之內部類的形式實現一個同步器類,也就是鎖,這個鎖繼承自AQS
    private static final class Sync extends AbstractQueuedSynchronizer {

        // 構造方法中指定鎖支持的線程數量
        Sync(int count) {
            // 若count小於0,則默認爲2
            if (count <= 0) {
                count = DEFAULT_SYNC_COUNT;
            }
            // 設置初始同步狀態
            setState(count);
        }
        
        /**
         * 重寫tryAcquireShared方法,這個方法用來修改同步狀態state,也就是獲取鎖
         */
        @Override
        protected int tryAcquireShared(int arg) {
            // 循環嘗試
            for (; ; ) {
                // 獲取當前的同步狀態
                int nowState = getState();
                // 計算當前線程獲取鎖後,新的同步狀態
                // 注意這裏使用了減法,由於此時的state表示的是還能支持多少個線程
                // 而當前線程若是得到了鎖,則state就要減少
                int newState = nowState - arg;
                
                // 若是newState小於0,表示當前已經沒有剩餘的資源了
                // 則當前線程不能獲取鎖,此時將直接返回小於0的newState;
                // 或者newState>0,就會執行compareAndSetState方法修改state的值,
                // 若修改爲功將,將返回大於0的newState;
                // 若修改失敗,則表示有其餘線程也在嘗試修改state,此時循環一次後,再次嘗試
                if (newState < 0 || compareAndSetState(nowState, newState)) {
                    return newState;
                }
            }
        }

        /**
         * 嘗試釋放同步狀態
         */
        @Override
        protected boolean tryReleaseShared(int arg) {
            for (; ; ) {
                // 獲取當前同步狀態
                int nowState = getState();
                // 計算釋放後的新同步狀態,這裏使用加法,
                // 表示有線程釋放鎖後,當前鎖能夠支持的線程數量增長了
                int newState = nowState + arg;
                // 使用CAS修改同步狀態,若成功則返回true,不然自旋
                if (compareAndSetState(nowState, newState)) {
                    return true;
                }
            }
        }
        
    }


    /**
     * 獲取鎖的方法
     */
    @Override
    public void lock() {
        // 這裏調用的是AQS的模板方法acquireShared,
        // 在acquireShared中將調用咱們重寫的tryAcquireShared方法
        // 傳入參數爲1表示當前線程,當前線程獲取鎖後,state將-1
        sync.acquireShared(1);
    }

    /**
     * 解鎖
     */
    @Override
    public void unlock() {
        // 這裏調用的是AQS的模板方法releaseShared,
        // 在acquireShared中將調用咱們重寫的tryReleaseShared方法
        // 傳入參數爲1表示當前線程,當前線程釋放鎖後,state將+1
        sync.releaseShared(1);
    }

    /*******************其餘須要實現的方法省略***************************/

}

  以上就實現了一個支持兩個線程同時容許的共享鎖,下面咱們經過一個測試代碼來測試效果:

public static void main(String[] args) throws InterruptedException {
	// 建立一個咱們自定義的鎖對象
    Lock lock = new TwinLock();

    // 啓動10個線程去嘗試獲取鎖
    for (int i = 0; i < 10; i++) {
        Thread t = new Thread(()->{
            // 循環執行
            while (true) {
                // 獲取鎖
                lock.lock();
                try {
                    // 休眠1秒
                    Thread.sleep(1000);
                    // 輸出線程名稱
                    System.out.println(Thread.currentThread().getName());
                    // 再次休眠一秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 釋放鎖
                    lock.unlock();
                }
            }
        });
		// 將線程設置爲守護線程,主線程結束後,收穫線程自動結束
        t.setDaemon(true);
        t.start();
    }

	// 主線程每隔1秒輸出一個分割行
    for (int i = 0; i < 10; i++) {
        Thread.sleep(1000);
        System.out.println("********************************");
    }
}

  以上測試代碼運行後,在每兩個分割行之間,最多不會輸出超過兩個線程的名稱,線程名稱的輸出將會以兩個一隊出現。個人輸出結果以下:

********************************
Thread-1
Thread-0
********************************
********************************
Thread-2
Thread-1
********************************
********************************
Thread-2
Thread-1
********************************
********************************
Thread-2
Thread-3
********************************
********************************
Thread-3
Thread-4
********************************

2.8 AQS如何實現線程等待

  在研究AQS的過程當中,我一直有這個疑惑——AQS如何讓線程阻塞,直到最後才知道有一個叫LockSupport的工具類。這個工具類定義了不少靜態方法,當須要讓一個阻塞,或者喚醒一個線程時,就能夠調用這個類中的方法,它的底層實現是經過一個sun.misc.Unsafe類的對象,unsafe類的方法都是本地方法,由其餘語言實現,這個類是給不支持地址操做的Java,提供的一個操做內存地址的後門。

  AQS中經過如下兩個方法來阻塞和喚醒線程:

  • LockSupport.park():阻塞當前線程;
  • LockSupport.unpark(Thread thread):將參數中傳入的線程喚醒;

  前面講解AQS的代碼中,用到了方法unparkSuccessor,它的主要做用就是喚醒當前節點的下一個節點對應的線程,咱們能夠看看它的部分實現:

private void unparkSuccessor(Node node) {

    // ...........省略其餘代碼............
    
    // 如下代碼即爲喚醒當前節點的下一個節點對應的線程
    Node s = node.next;
    if (s != null)
        LockSupport.unpark(s.thread);	// 使用LockSupport
}

3、總結

  其實AQS還支持一些其餘的方法,好比說在獲取鎖時設置超時時間等,這些方法的實現與上面介紹的幾種大同小異,限於篇幅,這裏就不進行敘述了。以上內容對AQS的實現原理以及主要方法的實現作了一個比較細緻的介紹,相信看完以後會對AQS有一個比較深刻的理解,可是想要理解以上內容,須要具有併發的一些基礎知識,好比說線程的狀態,CAS機制等。最後但願這篇博客對須要的人有所幫助吧。


4、參考

相關文章
相關標籤/搜索