(六)手撕并發編程之基于Semaphore與CountDownLatch分析AQS共享模式實現

引言

在上篇文章深入剖析并發之AQS獨占鎖&重入鎖(ReetrantLock)及Condition實現原理中我們曾基于ReetrantLock鎖分析了AQS獨占模式的實現原理,本章則準備從Semaphore信號量的角度出發一探AQS共享模式的具體實現。共享模式與獨占模式區別在于:共享模式下允許多條線程同時獲取鎖資源,而在之前分析的獨占模式中,在同一時刻只允許一條線程持有鎖資源。

一、快速認識Semaphore信號量及實戰

Semaphore信號量是java.util.concurrent(JUC)包下的一個并發工具類,可以用來控制同一時刻訪問臨界資源(共享資源)的線程數,以確保訪問臨界資源的線程能夠正確、合理的使用公共資源。而其內部則于ReetrantLock一樣,都是通過直接或間接的調用AQS框架的方法實現。在Semaphore中存在一個“許可”的概念:

在初始化Semaphore信號量需要為這個許可傳入一個數值,該數值表示表示同一時刻可以訪問臨界資源的最大線程數,也被稱為許可集。一條線程想要訪問臨界資源則需要先執行acquire()獲取一個許可,如果線程在獲取時許可集已經被分配完了,那么該線程則會進入阻塞等待狀態,直至有其他持有許可的線程釋放后才有可能獲取到許可。當線程訪問完成臨界資源后則需要執行release()方法釋放已獲取的許可。

其實通過如上這段描述,我們不難發現,Semaphore信號量里面的“許可”概念與前面我們文章中,分析的互斥鎖中的“同步狀態標識”有著異曲同工之妙,其實也就是我們所談的“鎖資源”。下面我們可以簡單看看Semaphore類中所提供的方法:

// 調用該方法后線程會從許可集中嘗試獲取一個許可
public void acquire()

// 線程調用該方法時會釋放已獲取的許可
public void release()

// Semaphore構造方法:permits→許可集數量
Semaphore(int permits) 

// Semaphore構造方法:permits→許可集數量,fair→公平與非公平
Semaphore(int permits, boolean fair) 

// 從信號量中獲取許可,該方法不響應中斷
void acquireUninterruptibly() 

// 返回當前信號量中未被獲取的許可數
int availablePermits() 

// 獲取并返回當前信號量中立即未被獲取的所有許可
int drainPermits() 

// 返回等待獲取許可的所有線程Collection集合
protected  Collection<Thread> getQueuedThreads();

// 返回等待獲取許可的線程估計數量
int getQueueLength() 

// 查詢是否有線程正在等待獲取當前信號量中的許可
boolean hasQueuedThreads() 

// 返回當前信號量的公平類型,如為公平鎖返回true,非公平鎖為false
boolean isFair() 

// 獲取當前信號量中一個許可,當沒有許可可用時直接返回false不阻塞線程
boolean tryAcquire() 

// 在給定時間內獲取當前信號量中一個許可,超時還未獲取成功則返回false
boolean tryAcquire(long timeout, TimeUnit unit) 

如上便是Semaphore信號量提供的一些主要方法,下面我們可以上個簡單小案例演示,需求如下:

現在項目中有個需求,每晚需要長時間處理大量的Excel表數據與數據庫中數據對賬請求,由于文件讀取屬于IO密集型任務,我們可以使用多線程的方式優化,加速處理速度。但是在該項目中因為還有其他業務要處理,為了保證整體性能,所以對于該業務的實現最多只能使用三個數據庫連接對象。因為如果當前業務線程同一時刻獲取的數據庫連接數量過多,會導致其他業務線程需要操作數據庫時獲取不到連接對象阻塞(因為數據庫連接對象與線程對象一樣數據珍惜資源/資源有限),從而引發整體程序堆積大量客戶端請求導致系統整體癱瘓。這時我們就需要控制同一時刻最多只有三條線程拿到數據庫連接進行操作,此時就可以使用Semaphore做流量控制。

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 自定義線程池(后續文章會詳細分析到)
        // 環境:四核四線程CPU 任務阻塞系數0.9
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                4*2, 40,
                30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(1024*10),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        // 設置信號量同一時刻最大線程數為3
        final Semaphore semaphore = new Semaphore(3);
        // 模擬100個對賬請求
        for (int index = 0; index < 100; index++) {
            final int serial = index;
            threadPool.execute(()->{
                try {
                    // 使用acquire()獲取許可
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() +
                            "線程成功獲取許可!請求序號: " + serial);
                    // 模擬數據庫IO
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }  finally {
                    // 臨界資源訪問結束后釋放許可
                    semaphore.release();
                }
            });
        }
        // 關閉線程池資源
        threadPool.shutdown();
    }
}

上述代碼中,在創建Semaphore信號量對象時為該對象初始化了三個許可,也就意味著在同一時刻允許三條線程同時訪問臨界資源。線程在訪問臨界資源之前,需要使用acquire()先成功獲取一個許可,才能訪問臨界資源。如果一條線程獲取許可,該信號量對象的許可集已經被分配完時,新來的線程需進入等待狀態。之前獲取許可成功的線程在操作完成之后需執行release()方法釋放已獲取的許可。我們執行如上案例則可看到執行結果幾乎每隔一千毫秒會出現三條線程同時訪問,如下:

/*
第一秒:程序運行初
  pool-1-thread-1線程成功獲取許可!請求序號: 0
  pool-1-thread-2線程成功獲取許可!請求序號: 1
  pool-1-thread-3線程成功獲取許可!請求序號: 2
第二秒:程序運行1000ms后
  pool-1-thread-4線程成功獲取許可!請求序號: 3
  pool-1-thread-5線程成功獲取許可!請求序號: 4
  pool-1-thread-6線程成功獲取許可!請求序號: 5
第三秒:程序運行2000ms后
  pool-1-thread-7線程成功獲取許可!請求序號: 6
  pool-1-thread-8線程成功獲取許可!請求序號: 7
  pool-1-thread-2線程成功獲取許可!請求序號: 8
第四秒:程序運行3000ms后
  ........
*/

如上便是一個簡單使用Demo,總體看來關于Semaphore信號量的用法還是比較簡單的。不過我們也在前面提到過這么一句話:

Semaphore信號量里的“許可”概念與前面我們文章中分析的互斥鎖的“同步狀態標識”有著異曲同工之妙。

那我們能否使用Semaphore信號量實現一把獨占鎖呢?答案也是肯定的,可以。我們只需要在創建信號量對象時,只給許可集分配一個數量即可,如下:

final Semaphore semaphore = new Semaphore(1);

二、Semaphore信號量中AQS的共享模式實現

Semaphore信號量其實與我們上篇文章所分析的ReetrantLock類結構大致相同,其內部存在繼承自AbstractQueuedSynchronizer內部Sync類以及它的兩個子類:FairSync公平鎖類和NofairSync非公平鎖類,從這我們也可以看出,Semaphore的內部實現其實與ReetrantLock一樣都是基于AQS組件實現的。在上一篇文章中我們也曾提到,AQS設計的初衷并不打算直接作為調用類對外暴露服務,而只是作為并發包基礎組件,為其他并發工具類提供基礎設施,如維護同步隊列、控制/修改同步狀態等。具體的獲取鎖和釋放鎖的邏輯則交給子類自己去實現,從而也能最大程度的保留框架的靈活性。因此無論是Semaphore還是ReetrantLock都需要獨自實現tryAcquireShared(int arg)獲取鎖方法以及tryReleaseShared(int arg)釋放鎖方法。AQS總體類圖關系如下:

AQS DML類圖結構

如上圖,Semaphore與ReetrantLock的結構大致相同,而實現思路也大致相同,獲取鎖(許可)的方法tryAcquireShared(arg)分別由兩個子類FairSync和NofairSync實現,因為公平鎖和非公平鎖的加鎖方式畢竟存在些許不同,而釋放鎖tryReleaseShared(arg)的邏輯則交由Sync實現,因為釋放操作都是相同的,因此放在父類Sync中實現自然是最好的方式。下面我們就從Semaphore源碼的角度分析AQS共享模式的具體實現原理,我們先從非公平鎖的獲取鎖實現開始。

2.1、AQS共享模式之Semaphore的NofairSync非公平鎖實現

我們在創建Semaphore對象時,也和ReetrantLock一樣手動選擇公平鎖和非公平鎖:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

通過Semaphore的構造函數我們不難發現,如果在我們創建時不選擇聲明公平類型,Semaphore默認創建的是非公平鎖類型,NonfairSync構造如下:

static final class NonfairSync extends Sync {
    // 構造函數:將給定的許可數permits傳給父類同步狀態標識state
    NonfairSync(int permits) {
          super(permits);
    }
   // 釋放鎖的方法實現則是直接調用父類Sync的釋放鎖方法
   protected int tryAcquireShared(int acquires) {
       return nonfairTryAcquireShared(acquires);
   }
}

從如上源碼中,我們可以得知Semaphore中的非公平鎖NonfairSync類的構造函數是基于調用父類Sync構造函數完成的,而在創建Semaphore對象時傳入的許可數permits最終則會傳遞給AQS同步器的同步狀態標識state,如下:

// 父類 - Sync類構造函數
Sync(int permits) {
    setState(permits); // 調用AQS內部的set方法
}

// AQS(AbstractQueuedSynchronizer)同步器
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer {
    // 同步狀態標識
    private volatile int state;
    
    protected final int getState() {
        return state;
    }
    protected final void setState(int newState) {
        state = newState;
    }
    // 對state變量進行CAS操作
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

從上述分析中可知,Semaphore對象創建時傳入的許可數permits,實則其實最終是在對AQS內部的state進行初始化。初始化完成后,state代表著當前信號量對象的可用許可數。

2.1.1、信號量中非公平鎖NonfairSync獲取許可/鎖實現

我們在使用Semaphore時獲取鎖是調用Semaphore.acquire()方法,,調用該方法的線程會開始獲取鎖/許可,嘗試對permits/state進行CAS加一,CAS成功則代表獲取成功。下面我們來分析一下Semaphore獲取許可的方法acquire()的具體實現,源碼如下:

// Semaphore類 → acquire()方法
public void acquire() throws InterruptedException {
      // Sync類繼承AQS,此處直接調用AQS內部的acquireSharedInterruptibly()方法
      sync.acquireSharedInterruptibly(1);
  }

// AbstractQueuedSynchronizer類 → acquireSharedInterruptibly()方法
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 判斷是否出現線程中斷信號(標志)
    if (Thread.interrupted())
        throw new InterruptedException();
    // 如果tryAcquireShared(arg)執行結果不小于0,則線程獲取同步狀態成功
    if (tryAcquireShared(arg) < 0)
        // 未獲取成功加入同步隊列阻塞等待
        doAcquireSharedInterruptibly(arg);
}

信號量獲取許可的方法acquire()最終是通過Sync對象調用AQS內部的acquireSharedInterruptibly()方法完成的,而acquireSharedInterruptibly()在獲取同步狀態標識的過程中是可以響應線程中斷操作的,如果該操作沒有沒中斷,則首先調用tryAcquireShared(arg)嘗試獲取一個許可數,獲取成功則返回執行業務,方法結束。如果獲取失敗,則調用doAcquireSharedInterruptibly(arg)將當前線程加入同步隊列阻塞等待。不過值得我們注意的是:tryAcquireShared(arg)方法是AQS提供的模板方法,并沒有提供具體實現,而是把具體實現的邏輯交由子類完成,我們先看看信號量中非公平鎖NonfairSync類的實現:

    // Semaphore類 → NofairSync內部類 → tryAcquireShared()方法
protected int tryAcquireShared(int acquires) {
    // 調用了父類Sync中的實現方法
    return nonfairTryAcquireShared(acquires);
}

// Syn類 → nonfairTryAcquireShared()方法
abstract static class Sync extends AbstractQueuedSynchronizer {
    final int nonfairTryAcquireShared(int acquires) {
         // 開啟自旋死循環
         for (;;) {
             int available = getState();
             int remaining = available - acquires;
             // 判斷信號量中可用許可數是否已<0或者CAS執行是否成功
             if (remaining < 0 ||
                 compareAndSetState(available, remaining))
                 return remaining;
         }
     }
}

nonfairTryAcquireShared(acquires)方法首先獲取到state值后,減去一得到remaining值,如果不小于0則代表著當前信號量中還存在可用許可,當前線程開始嘗試cas更新state值,cas成功則代表獲取同步狀態成功,返回remaining值。反之,如果remaining值小于0則代表著信號量中的許可數已被其他線程獲取,目前不存在可用許可數,直接返回小于0的remaining值,nonfairTryAcquireShared(acquires)方法執行結束,回到AQS的acquireSharedInterruptibly()方法。當返回的remaining值小于0時,if(tryAcquireShared(arg)<0)條件成立,進入if執行doAcquireSharedInterruptibly(arg)方法將當前線程加入同步隊列阻塞,等待其他線程釋放同步狀態。線程入列方法如下:

// AbstractQueuedSynchronizer類 → doAcquireSharedInterruptibly()方法
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 創建節點狀態為Node.SHARED共享模式的節點并將其加入同步隊列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
     // 開啟自旋操作
     for (;;) {
         final Node p = node.predecessor();
         // 判斷前驅節點是否為head
         if (p == head) {
             // 嘗試獲取同步狀態state
             int r = tryAcquireShared(arg);
             // 如果r不小于0說明獲取同步狀態成功
             if (r >= 0) {
                 // 將當前線程結點設置為頭節點并喚醒后繼節點線程             
                 setHeadAndPropagate(node, r);
                 p.next = null; // 置空方便GC
                 failed = false;
                 return;
             }
         }
       // 調整同步隊列中node節點的狀態并判斷是否應該被掛起 
       // 并判斷是否存在中斷信號,如果需要中斷直接拋出異常結束執行
         if (shouldParkAfterFailedAcquire(p, node) &&
             parkAndCheckInterrupt())
             throw new InterruptedException();
     }
    } finally {
     if (failed)
         // 結束該節點線程的請求
         cancelAcquire(node);
    }
}

// AbstractQueuedSynchronizer類 → setHeadAndPropagate()方法
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // 獲取同步隊列中原本的head頭節點
    setHead(node); // 將傳入的node節點設置為頭節點
    /*
     * propagate=剩余可用許可數,h=舊的head節點
     * h==null,(h=head)==null:
     *      非空判斷的標準寫法,避免原本head以及新的頭節點node為空
     * 如果當前信號量對象中剩余可用許可數大于0或者
     * 原本頭節點h或者新的頭節點node不是結束狀態則喚醒后繼節點線程
     * 
     * 寫兩個if的原因在于避免造成不必要的喚醒,因為很有可能喚醒了后續
     * 節點的線程之后,還沒有線程釋放許可/鎖,從而導致再次陷入阻塞
     */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 避免傳入的node為同步隊列的唯一節點,
        // 因為隊列中如果只存在node一個節點,那么后驅節點s必然為空
        if (s == null || s.isShared())
            doReleaseShared(); // 喚醒后繼節點
    }
}

doAcquireSharedInterruptibly(arg)方法中總共做了三件事:

  • 一、創建一個狀態為Node.SHARED共享模式的節點,并通過addWaiter()加入隊列
  • 二、加入成功后開啟自旋,判斷前驅節點是否為head,是則嘗試獲取同步狀態標識,獲取成功后,將自己設置為head節點,如果可用許可數大于0則喚醒后繼節點的線程
  • 三、如果前驅節點不為head的節點以及前驅節點為head節點但獲取同步狀態失敗的節點,則調用shouldParkAfterFailedAcquire(p,node)判斷前驅節點的狀態是否為SIGNAL狀態(一般shouldParkAfterFailedAcquire(p,node)中的for循環至少需要執行兩次以上才會返回ture,第一次把前驅節點設置為SIGNAL狀態,第二次檢測到SIGNAL狀態),如果是則調用parkAndCheckInterrupt()掛起當前線程并返回線程中斷狀態

如上便是doAcquireSharedInterruptibly(arg)方法的大概工作,接下來我們可以看看shouldParkAfterFailedAcquire()以及parkAndCheckInterrupt()方法:

// AbstractQueuedSynchronizer類 → shouldParkAfterFailedAcquire()方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 獲取當前節點的等待狀態
    int ws = pred.waitStatus;
    // 如果為等待喚醒(SIGNAL)狀態則返回true
    if (ws == Node.SIGNAL)
        return true;
    // 如果當前節點等待狀態大于0則說明是結束狀態,
    // 遍歷前驅節點直到找到沒有結束狀態的節點
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果當前節點等待狀態小于0又不是SIGNAL狀態,
        // 則將其設置為SIGNAL狀態,代表該節點的線程正在等待喚醒
        // 也就是代表節點是剛從Condition的條件等待隊列轉移到同步隊列,
        // 節點狀態為CONDITION狀態(Semaphore中不存在condition的概念,
        // 所以同步隊列不會出現這個狀態的節點,此處代碼不會執行)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

// AbstractQueuedSynchronizer類 → parkAndCheckInterrupt()方法
private final boolean parkAndCheckInterrupt() {
    // 將當前線程掛起
    LockSupport.park(this);
    // 獲取線程中斷狀態,interrupted()是判斷當前中斷狀態,
    // 而并不是中斷線程,線程需要中斷返回true,反之false
    return Thread.interrupted();
}

LockSupport → park()方法
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    // 設置當前線程的監視器blocker
    setBlocker(t, blocker);
    // 調用了native方法到JVM級別的阻塞機制阻塞當前線程
    UNSAFE.park(false, 0L);
    // 阻塞結束后把blocker置空
    setBlocker(t, null);
}

shouldParkAfterFailedAcquire()方法的作用是判斷節點的前驅節點是否為等待喚醒狀態(SIGNAL狀態),如果是則返回true。如果前驅節點的waitStatus大于0(只有CANCELLED結束狀態=1>0),既代表該前驅節點已沒有用了,應該從同步隊列移除,執行do/while循環遍歷所有前前驅節點,直到尋找到非CANCELLED結束狀態的節點。如果節點狀態為SIGNAL等待喚醒狀態則直接調用parkAndCheckInterrupt()掛起當前線程。至此整個Semaphore.acquire()獲取許可的方法流程結束。如下圖:

AQS之圖解共享式獲取鎖過程

如上圖,在AQS同步器中存在一個變量state,Semaphore信號量對象在初始化時傳遞的permits許可數會間接的賦值給AQS中的state同步標識,而permits/state則代表著同一時刻可同時訪問臨界/共享資源的最大線程數。當一條線程調用Semaphore.acquire()獲取許可時,會首先判斷state是否大于0,如果大于則代表還有可用許可數,state減1,線程獲取成功并返回執行。直到state為零時,代表著當前信號量已經不存在可用許可數了,后續請求的線程則需要封裝成Node節點并將其加入同步隊列開啟自旋操作直至有線程釋放許可(state加一)。

至此,AQS共享模式中非公平鎖的獲取鎖原理分析完畢。但是我們如上分析的是可響應線程中斷請求的獲取許可方式,而Semaphore中也實現了一套不可中斷式的獲取方法,如下:

// Semaphore類 → acquireUninterruptibly()方法
public void acquireUninterruptibly() {
    sync.acquireShared(1);
}

// AbstractQueuedSynchronizer類 → acquireShared()方法
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

// AbstractQueuedSynchronizer類 → doAcquireShared()方法
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 在前面的可中斷式獲取鎖方法中此處是直接拋出異常強制中斷線程的
                // 而在不可中斷式的獲取方法中,這里是沒有拋出異常中斷線程的
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

觀察如上源碼不難發現,可響應線程中斷的方法與不可響應線程中斷的方法區別在于:

可響應線程中斷的方法在每次操作之前會先檢測線程中斷信號,如果線程需要中斷操作,則直接拋出異常強制中斷線程的執行。反之,不可響應線程中斷的方法不會檢測線程中斷信號,而且不會拋出異常強制中斷。

2.1.2、信號量中非公平鎖NonfairSync釋放許可/鎖實現

使用Semaphore時釋放鎖則調用的是Semaphore.release()方法,調用該方法之后線程持有的許可會被釋放,同時permits/state加一,接下來Semaphore獲取許可的方法release()的具體實現,源碼如下:

// Semaphore類 → release()方法
public void release() {
    sync.releaseShared(1);
}

// AbstractQueuedSynchronizer類 → releaseShared(arg)方法
public final boolean releaseShared(int arg) {
    // 調用子類Semaphore中tryReleaseShared()方法實現
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

與之前獲取許可的方法一樣,Semaphore釋放許可的方法release()也是通過間接調用AQS內部的releaseShared(arg)完成。因為AQS的releaseShared(arg)是魔法方法,所以最終的邏輯實現由Semaphore的子類Sync完成,如下:

// Semaphore類 → Sync子類 → tryReleaseShared()方法
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        // 獲取AQS中當前同步狀態state值
        int current = getState();
        // 對當前的state值進行增加操作
        int next = current + releases;
        // 不可能出現,除非傳入的releases為負數
        if (next < current) 
            throw new Error("Maximum permit count exceeded");
        // CAS更新state值為增加之后的next值
        if (compareAndSetState(current, next))
            return true;
    }
}

釋放鎖/許可的方法邏輯相對來說比較簡單,對AQS中的state加一釋放獲取的同步狀態。不過值得注意的是:在我們上篇文章分享的AQS獨占模式實現中,釋放鎖的邏輯中是沒有保證線程安全的,因為獨占模式的釋放鎖邏輯永遠只會存在一條線程同時操作。而在共享模式中,可能會存在多條線程同時釋放許可/鎖資源,所以在此處使用了CAS+自旋的方式保證線程安全問題。

如果此處tryReleaseShared(releases)CAS更新成功,那么則會進入if(tryReleaseShared(arg))中執行doReleaseShared();喚醒后繼節點線程。

// AbstractQueuedSynchronizer類 → doReleaseShared()方法
private void doReleaseShared() {
    /*
     * 為了防止釋放過程中有其他線程進入隊列,這里必須開啟自旋
     * 如果頭節點設置失敗則重新檢測繼續循環
     */
    for (;;) {
        // 獲取隊列head頭節點
        Node h = head; 
        // 如果頭節點不為空并且隊列中還存在其他節點
        if (h != null && h != tail) { 
            // 獲取頭節點的節點狀態
            int ws = h.waitStatus; 
            // 如果節點狀態為SIGNAL等待喚醒狀態則代表
            if (ws == Node.SIGNAL) { 
                // 嘗試cas修改節點狀態值為0
                // 失敗則繼續下次循環
                // 成功則喚醒頭節點的后繼節點
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;         
                unparkSuccessor(h);  // 喚醒后繼節點線程
            }
            // 節點狀態為0時嘗試將節點狀態修改為PROPAGATE傳播狀態
            // 失敗則跳出循環繼續下次循環
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;               
        }
        // 如果當前隊列頭節點發生變化繼續循環,反之則終止自旋
        if (h == head)
            break;
    }
}
// AbstractQueuedSynchronizer類 → unparkSuccessor()方法
// 參數:傳入需要喚醒后繼節點的節點
private void unparkSuccessor(Node node) {
    // 獲取node節點的線程狀態
    int ws = node.waitStatus;
    if (ws < 0)
        // 設置head節點為0
        compareAndSetWaitStatus(node, ws, 0);
    // 獲取后繼節點
    Node s = node.next;
    // 如果后繼節點為空或線程狀態已經結束
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 遍歷整個隊列拿到可喚醒的節點
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 喚醒后繼節點線程
        LockSupport.unpark(s.thread);
}

doReleaseShared()方法直接在執行中獲取head節點,通過調用unparkSuccessor()方法喚醒head后繼節點中的線程。而因為該方法體的邏輯是一個for(;;){}死循環,退出的條件為:只當隊頭不發生改變時才退出,如果發生改變了則代表著一定有其他線程在當前線程釋放鎖/許可的過程中獲取到了鎖,所以當前循環會繼續,在第二次循環過程中,在滿足條件h.waitStauts==0的情況下,這里會把head節點的waitStauts設置為Node.PROPAGATE傳播狀態是為了保證喚醒傳遞。因為AQS共享模式下是會出現多個線程同時對同步狀態標識state進行操作,如線程T1在執行release()→doReleaseShared()釋放許可操作,剛喚醒后繼線程準備替換為head頭節點(準備替換但是還沒替換),此時另外一條線程T2正好在同一時刻執行acquire()→doAcquireShared()→setHeadAndPropagate()獲取鎖操作,假設T2線程獲取的是最后一個可用許可,在執行到setHeadAndPropagate()方法(這個方法中存在一個超長判斷),傳入的propagate=0

// AbstractQueuedSynchronizer類 → setHeadAndPropagate()方法
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // 獲取同步隊列中原本的head頭節點
    setHead(node); // 將傳入的node節點設置為頭節點
    /*
     * propagate=剩余可用許可數,h=舊的head節點
     * h==null,(h=head)==null:
     *      非空判斷的標準寫法,避免原本head以及新的頭節點node為空
     * 如果當前信號量對象中剩余可用許可數大于0或者
     * 原本頭節點h或者新的頭節點node不是結束狀態則喚醒后繼節點線程
     * 
     * 寫兩個if的原因在于避免造成不必要的喚醒,因為很有可能喚醒了后續
     * 節點的線程之后,還沒有線程釋放許可/鎖,從而導致再次陷入阻塞
     */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 避免傳入的node為同步隊列的唯一節點,
        // 因為隊列中如果只存在node一個節點,那么后驅節點s必然為空
        if (s == null || s.isShared())
            doReleaseShared(); // 喚醒后繼節點
    }
}

// 超長判斷:該判斷的作用在于面對各種特殊情況能夠時保證及時獲取鎖
if (propagate > 0 || h == null || h.waitStatus < 0 ||
    (h = head) == null || h.waitStatus < 0) {
    Node s = node.next;
    // 避免傳入的node為同步隊列的唯一節點,
    // 因為隊列中如果只存在node一個節點,那么后驅節點s必然為空
    if (s == null || s.isShared())
        doReleaseShared(); // 喚醒后繼節點
}

根據這個超長判斷的邏輯,因為傳入的propagate=0代表著當前已經沒有可用許可數了,不滿足超長判斷中的第一個條件propagate>0,所以T2線程獲取鎖之后理論上是不需要再喚醒其他線程獲取鎖/許可,但是因為T1線程已經訪問完成臨界資源了正在釋放持有的許可,那么就會造成一種情況:隊列中head節點的后繼節點如果此時嘗試獲取鎖/許可,那么有很大幾率獲取到T1線程釋放的許可。所以在釋放鎖時,將head節點的waitStauts設置為Node.PROPAGATE傳播狀態值為-3,滿足這個超長判斷中的第三個條件h.waitStatus < 0,所以此時T2也會喚醒head后繼節點中等待獲取鎖/許可資源的線程。這樣去實現的好處在于:能夠充分照顧到head的后繼節點同時也能保證喚醒的傳遞。

至于這里為什么獲取到鎖/許可的線程需要繼續喚醒后繼節點線程?因為這里是共享鎖,而不是獨占鎖。一個線程剛獲得了共享鎖/許可,那么很有可能還有剩余的共享鎖可供排隊在后面的線程獲得,所以需要喚醒后面的線程。

至此,釋放許可邏輯結束,對比獲取許可的邏輯相對來說要簡單許多,只需要更新state值后調用doReleaseShared()方法喚醒后繼節點線程即可。但是在理解doReleaseShared()方法時需要額外注意:調用doReleaseShared()方法的線程會存在兩種:

  • 一是釋放共享鎖/許可數的線程。調用release()方法釋放許可時必然調用它喚醒后繼線程
  • 二是剛獲取到共享鎖/許可數的線程。一定情況下,在滿足“超長判斷”的任意條件時也會調用它喚醒后繼線程

2.2、AQS共享模式之Semaphore的FairSync公平鎖實現

AQS共享模式中的公平鎖實現除開在獲取鎖的邏輯上與非公平鎖的有些許不同外,其他的實現大致相同。

2.2.1、信號量中公平鎖FairSync獲取許可/鎖實現

公平鎖的概念是指先請求鎖的線程一定比后請求鎖的線程要先執行,先獲取到鎖資源,從時間上需要保證執行的先后順序。

公平鎖獲取許可執行邏輯:Semaphore.acquire()獲取許可方法 → AQS.acquireSharedInterruptibly()方法 → AQS.tryAcquireShared()獲取共享鎖模板方法 → FairSync.tryAcquireShared()方法

// Semaphore類 → 構造方法
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
// Semaphore類 → acquire()獲取鎖方法
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// AbstractQueuedSynchronizer類 → acquireSharedInterruptibly()方法
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 調用AQS定義的獲取共享鎖的模板方法
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
// AbstractQueuedSynchronizer類 → tryAcquireShared()模板方法
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

// Semaphore類 → FairSync公平鎖類
static final class FairSync extends Sync {
    FairSync(int permits) {
        super(permits);
    }
    
    // Semaphore類 → FairSync內部類 → tryAcquireShared()子類實現
    protected int tryAcquireShared(int acquires) {
        for (;;) {
            // 不同點:先判斷隊列中是否存在節點后再執行獲取鎖操作
            if (hasQueuedPredecessors())
                return -1;
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
}

從獲取鎖/許可的代碼中可以非常明顯的看出,公平鎖的實現相對來說,與非公平鎖的唯一不同點在于:公平鎖的模式下獲取鎖,會先調用hasQueuedPredecessors()方法判斷同步隊列中是否存在節點,如果存在則直接返回-1回到acquireSharedInterruptibly()方法if(tryAcquireShared(arg)<0)判斷,調用doAcquireSharedInterruptibly(arg)方法將當前線程封裝成Node.SHARED共享節點加入同步隊列等待。反之,如果隊列中不存在節點則嘗試直接獲取鎖/許可。

2.2.2、信號量中公平鎖FairSync釋放許可/鎖實現

公平鎖釋放許可的邏輯與非公平鎖的實現是一致的,因為都是Sync類的子類,而釋放鎖的邏輯都是對state減一更新后,喚醒后繼節點的線程。所以關于釋放鎖的具體實現則是交由Sync類實現,這里不再重復贅述。

三、ReetrantLock與Semaphore的區別

對比項 ReetrantLock Semaphore
實現模式 獨占模式 共享模式
獲取鎖方法 tryAcquire() tryAcquireShared()
釋放鎖方法 tryRelease() tryAcquireShared()
是否支持重入 支持 不支持
線程中斷 支持 支持
Condition 支持 不支持
隊列數量 一個同步+多個等待 單個同步
節點類型 Node.EXCLUSIVE Node.SHARED

四、共享模式的其他實現者

除開Semaphore信號量的實現是基于AQS的共享模式之外,在JUC并發包中CountDownLatch、ReetrantReadWriteLock讀寫鎖的Read讀鎖等都是基于AQS的共享模式實現的,下面我們也可以簡單的看看關于CountDownLatch的用法。

4.1、CountDownLatch應用場景實戰

在CountDownLatch初始化時和Semaphore一樣,我們需要傳入一個數字count作為最大線程數

CountDownLatch countDownLatch = new CountDownLatch(3);

這個參數同樣會間接的賦值給AQS內部的state同步狀態標識。一般我們會調用它的兩個方法:await()與countDown()

  • await():調用await()方法的線程會被封裝成共享節點加入同步隊列阻塞等待,直至state=0時才會喚醒同步隊列中所有的線程
  • countDown():調用countDown()方法的線程會對state減一

而關于CountDownLatch有兩種用法:

  • 一、多等一:初始化count=1,多條線程await()阻塞,一條線程調用countDown()喚醒所有阻塞線程
  • 二、一等多:初始化count=x,多線程countDown()對count進行減一,一條線程await()阻塞,當count=0時阻塞的線程開始執行

如上兩種用法在我們的項目中也可以有很多的應用場景,多等一的用法我們可以用來在一定程度上模擬并發測試接口并發安全問題、死鎖問題等,如:

final CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 1; i <= 3; i++) {
    new Thread(() -> {
        try {
            System.out.println("線程:" + Thread.currentThread().getName()
            + "....阻塞等待!");
            countDownLatch.await();
            // 可以在此處調用需要并發測試的方法或接口
            System.out.println("線程:" + Thread.currentThread().getName()
            + "....開始執行!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "T" + i).start();
}
Thread.sleep(1000);
countDownLatch.countDown();

/*
   程序開始運行:
    線程:T2....阻塞等待!
    線程:T1....阻塞等待!
    線程:T3....阻塞等待!
   程序運行一秒后(三條線程幾乎同時執行):
    線程:T2....開始執行!
    線程:T1....開始執行!
    線程:T3....開始執行!
*/

如上案例中,創建了一個CountDownLatch對象,初始化時傳遞count=1,循環創建三條線程T1,T2,T3阻塞等待,主線程在一秒后調用countDown()喚醒了同步隊列中的三條線程繼續執行,原理如下:

CountDownLatch多等一原理

我們在實際開發過程中,往往很多并發任務都存在前后依賴關系,如詳情頁需要調用多個接口完成數據聚合、并行執行獲取到數據后需要進行結果合并、多個操作完成后需要進行數據檢查等等,而這些場景下我們可以使用一等多的用法:

final CountDownLatch countDownLatch = new CountDownLatch(3);
Map data = new HashMap();
for (int i = 1; i <= 3; i++) {
    final int page = i;
    new Thread(() -> {
        System.out.println("線程:" + Thread.currentThread().getName() +
                "....讀取分段數據:"+(page-1)*200+"-"+page*200+"行");
        // 數據加入結果集:data.put();
        countDownLatch.countDown();
    }, "T" + i).start();
}
countDownLatch.await();
System.out.println("線程:" + Thread.currentThread().getName() 
        + "....對數據集:data進行處理");
        
/*
運行結果:
    線程:T1....讀取分段數據:0-200行
    線程:T2....讀取分段數據:200-400行
    線程:T3....讀取分段數據:400-600行

    線程main....對數據集:data進行處理
*/

如上一等多的案例中,for循環開啟三個線程T1,T2,T3并行執行同時讀取數據增快處理效率,讀取完成之后將數據加入data結果集中匯總,主線程等待三條線程讀取完成后對數據集data進行處理,如下:

CountDownLatch一等多原理

4.2、CountDownLatch實現原理

前面我們曾提到過,CountDownLatch也是基于AQS共享模式實現的,與Semaphore一樣,會將傳入的count間接的賦值給AQS內部的state同步狀態標識。

private final Sync sync;

// CountDownLatch構造方法
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    // 對其內部Sync對象進行初始化
    this.sync = new Sync(count);
}

// CountDownLatch類 → Sync內部類
private static final class Sync extends AbstractQueuedSynchronizer {
    // Sync構造函數:對AQS內部的state進行賦值
    Sync(int count) {setState(count);}
    
    // 調用await()方法最終會調用到這里
    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }
}

// CountDownLatch類 → await()方法
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

在CountDownLatch中,存在一個Sync內部類,當我們創建一個CountDownLatch對象時,實則其內部的構造函數是在對其sync對象進行初始化,與我們前面所說的一樣

CountDownLatch countDownLatch = new CountDownLatch(count);

初始化時傳遞的count數字最終會通過調用setState(state)方法賦值給AQS內部的同步狀態標識state變量,而當線程調用await()方法時,會調用AQS的acquireSharedInterruptibly()方法:

// AbstractQueuedSynchronizer類 → acquireSharedInterruptibly()方法
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 最終調用到CountDownLatch內部Sync類的tryAcquireShared()方法
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

// CountDownLatch類 → Sync內部類 → tryAcquireShared()方法
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

因為AQS中tryAcquireShared(arg)方法僅是模板方法的原因,所以線程在執行acquireSharedInterruptibly()方法時最終會調用到CountDownLatch內部Sync類的tryAcquireShared()方法。當count/state為0時返回true,反之,count不為0時返回false,最終回到if(tryAcquireShared(arg)<0)執行時,如果count不為0則執行doAcquireSharedInterruptibly(arg)方法將當前線程信息封裝成Node.SHARED共享節點加入同步隊列阻塞等待。原理如下:

CountDownLatch.await原理

如上便是CountDownLatch.await()的實現原理,大體來說還是比較簡單。下面我們接著分析一下CountDownLatch.countDown()方法的實現。

// CountDownLatch類 → countDown()方法
public void countDown() {
    sync.releaseShared(1);
}

// AbstractQueuedSynchronizer類 → releaseShared()方法
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

// AbstractQueuedSynchronizer類 → 模板方法:tryReleaseShared()
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

// CountDownLatch類 → Sync內部類 → tryReleaseShared()方法
// 調用countDown()方法最終會調用到這里
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

調用CountDownLatch.countDown()方法后會調用AQS的tryReleaseShared()模板方法,最終調用CountDownLatchSync內部類tryReleaseShared()方法。在該方法中首先會先對于state/count進行一次為0判斷,如果不為0則對count/state減一,然后再次對更新之后的state/count進行為0判斷,如果減一后state等于0返回true,回到releaseShared()if(tryReleaseShared(arg))執行doReleaseShared()喚醒同步隊列中的阻塞線程。反之,如果減一后不為0,當前線程則直接返回,方法結束。如下:

CountDownLatch.countDown原理

4.3、CountDownLatch與CyclicBarrier的區別

在JUC包中還存在另一個和CountDownLatch作用相同的工具類:CyclicBarrier。與CountDownLatch不同的是:CyclicBarrier是基于AQS的獨占模式實現的,其內部通過ReetrantLock與Condition實現線程阻塞與喚醒。對比如下:

對比項 CountDownLatch CyclicBarrier
實現模式 共享模式 獨占模式
計數方式 減法 減法
復用支持 不可復用 計數可置0
重置支持 不可重置 可重置
設計重點 一等多 多等多

五、總結

通過Semaphore與CountDownLatch原理進行分析后,不難得知,在初始化時傳遞的許可數/計數器最終都會間接的傳遞給AQS的同步狀態標識state。當一條線程嘗試獲取共享鎖時,會對state減一,當state為0時代表沒有可用共享鎖了,其他后續請求的線程會被封裝成共享節點加入同步隊列等待,直至其他持有共享鎖的線程釋放(state加一)。不過與獨占模式不同的是:共享模式中,除開釋放鎖時會喚醒后繼節點的線程外,獲取共享鎖成功的線程也會在滿足一定條件下喚醒后繼節點。至于共享模式中的公平鎖與非公平鎖則與之前的獨占模式的公平鎖與非公平鎖相同,公平鎖情況下,先判斷隊列是否存在Node再獲取鎖,從而保證每條線程獲取共享鎖時都是先到先得的順序執行的。而非公平鎖情況下,通過線程競爭的方式獲取,不管隊列中是否已經存在Node節點,請求的線程都會先執行一遍獲取鎖的邏輯,只要執行成功就能獲取到共享鎖,獲得線程執行權。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容