CountDownLatch 深度解析 由淺入深的瞭解高併發編程

目錄

一、CountDownLatch的應用場景

1、做併發性能測試

2、多線程執行任務,最後彙總

情景模擬

上代碼:

二、分析一下CountDownLatch底層實現

CountDownLatch我們如何接近你:

JDK1.8  CountDownLatch源碼:

CountDownLatch中的核心方法

三、線程的掛起和恢復#

unpark#

park#

四、CountDownLatch  的喚醒機制


 

AbstractQueuedSynchronizer,簡稱AQS。是一個用於構建鎖和同步器的框架,許多同步器都可以通過AQS很容易並且高效地構造出來,如常用的ReentrantLockSemaphoreCountDownLatch等。基於AQS來構建同步器能帶來許多好處。它不僅能極大地減少實現工作,而且也不必處理在多個位置上發生的競爭問題。在基於AQS構建的同步器中,只可能在一個時刻發生阻塞,從而降低上下文切換的開銷,並提高吞吐量。Doug Lea 大神在設計AQS時也充分考慮了可伸縮性,因此java.util.concurrent中所有基於AQS構建的同步器都能獲得這個優勢。大多數開發者都不會直接使用AQS,JUC中標準同步器類都能夠滿足絕大多數情況的需求。但如果能瞭解標準同步器類的實現方式,那麼對理解它們的工作原理是非常有幫助的。

 

一、CountDownLatch的應用場景

1、做併發性能測試

這是一種真正意義上的併發,不再是啓動一個線程,調用一下start()方法了,而是開啓線程後,讓線程在同一起跑線上開始競爭資源,測試代碼的併發能力。

2、多線程執行任務,最後彙總

由於業務比較複雜,每個功能比較獨立而且十分耗時,但是條件是等待每個功能都執行完畢後,主線程才能繼續向下執行。

 

針對於以上使用背景,我們可以模擬一種場景,將上述的兩種情況都覆蓋到:

情景模擬

 背景:
      今年學校組織了一場運動會,其中有一個項目是1000米跑步比賽。
  參賽人員:   
      在經過一系列的比賽過後,最後決賽就只剩下了5個人,他們分別是:
      張三
      李四
      王五
      趙六
 *    田七
      他們開始了最終的決賽。
 比賽要求:
     1、聽發令槍後開始跑。
     2、不得搶跑。
     3、最後由裁判統計好結果後比賽結束。

上代碼:

運動員類: 

package org.hcgao.common.blog.AQS.countdownlatch;

import java.util.concurrent.CountDownLatch;

public class Sportsman implements Runnable {
    	
    	private String sportsmanName;
    	// 發令槍信號
    	private CountDownLatch firingGunSignal;
    	// 賽場所有運動員都到達目的地
    	private CountDownLatch answer;
    	
    	public Sportsman(String sportsmanName, CountDownLatch firingGunSignal, CountDownLatch answer) {
    		this.sportsmanName = sportsmanName;
    		this.firingGunSignal = firingGunSignal;
    		this.answer = answer;
    	}

		@Override
		public void run() {
			 try {
				System.out.println("選手--" + sportsmanName + "--準備就緒,正在等待裁判發佈口令");
				firingGunSignal.await();
	            System.out.println("選手--" + sportsmanName + "--已接受裁判口令");
	            long time = (long) (Math.random() * 10000);
	            Thread.sleep(time);
	            System.out.println("選手--" + sportsmanName + "--到達終點, 用時  :  "+ time + "毫秒");
	            answer.countDown();
			} catch (InterruptedException e) {
                e.printStackTrace();
            }
			
		}
    }

調用方法:

package org.hcgao.common.blog.AQS.countdownlatch;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 背景:
 *     今年學校組織了一場運動會,其中有一個項目是1000米跑步比賽。
 * 參賽人員:   
 *     在經過一系列的比賽過後,最後決賽就只剩下了5個人,他們分別是:
 *     張三
 *     李四
 *     王五
 *     趙六
 *     田七
 *     他們開始了最終的決賽。
 *比賽要求:
 *	1、聽發令槍後開始跑。
 *	2、不得搶跑。
 *	3、最後由裁判統計好結果後比賽結束。
 *     
 * @author gaohaicheng123
 *
 */
public class CountdownLatchGun {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final CountDownLatch firingGunSignal = new CountDownLatch(1);
        final CountDownLatch answer = new CountDownLatch(5);
        
        Sportsman zhangsan = new Sportsman("張三", firingGunSignal, answer);
        Sportsman lisi = new Sportsman("李四", firingGunSignal, answer);
        Sportsman wangwu = new Sportsman("王五", firingGunSignal, answer);
        Sportsman zhaoliu = new Sportsman("趙六", firingGunSignal, answer);
        Sportsman tianqi = new Sportsman("田七", firingGunSignal, answer);
        service.execute(zhangsan);
        service.execute(lisi);
        service.execute(wangwu);
        service.execute(zhaoliu);
        service.execute(tianqi);
        try {
            Thread.sleep((long) (Math.random() * 10000));
            System.out.println("裁判"+Thread.currentThread().getName()+"即將發射信號槍");
            firingGunSignal.countDown();
            System.out.println("裁判"+Thread.currentThread().getName()+"已發信號槍,正在等待所有選手到達終點");
            answer.await();
            System.out.println("所有運動員均到達了目的地,裁判得到了他們的比賽數據。");
            System.out.println("裁判"+Thread.currentThread().getName()+"彙總成績,進行排名,頒獎,比賽結束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
    
    
}

執行結果:

選手王五準備就緒,正在等待裁判發佈口令
選手張三準備就緒,正在等待裁判發佈口令
選手趙六準備就緒,正在等待裁判發佈口令
選手李四準備就緒,正在等待裁判發佈口令
選手田七準備就緒,正在等待裁判發佈口令
裁判main即將發射信號槍
選手王五已接受裁判口令
選手張三已接受裁判口令
選手李四已接受裁判口令
選手趙六已接受裁判口令
選手田七已接受裁判口令
裁判main已發信號槍,正在等待所有選手到達終點
選手趙六到達終點, 用時  :  225毫秒
選手王五到達終點, 用時  :  492毫秒
選手張三到達終點, 用時  :  1153毫秒
選手田七到達終點, 用時  :  4516毫秒
選手李四到達終點, 用時  :  6936毫秒
所有運動員均到達了目的地,裁判得到了他們的比賽數據。
裁判main彙總成績,進行排名,頒獎,比賽結束

 

二、分析一下CountDownLatch底層實現

 

CountDownLatch我們如何接近你:

 

 

JDK1.8  CountDownLatch源碼:

public class CountDownLatch {
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        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;
            }
        }
    }

    private final Sync sync;

    /**
     * Constructs a {@code CountDownLatch} initialized with the given count.
     *
     * @param count the number of times {@link #countDown} must be invoked
     *        before threads can pass through {@link #await}
     * @throws IllegalArgumentException if {@code count} is negative
     */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    /**
     * Causes the current thread to wait until the latch has counted down to
     * zero, unless the thread is {@linkplain Thread#interrupt interrupted}.
     *
     * <p>If the current count is zero then this method returns immediately.
     *
     * <p>If the current count is greater than zero then the current
     * thread becomes disabled for thread scheduling purposes and lies
     * dormant until one of two things happen:
     * <ul>
     * <li>The count reaches zero due to invocations of the
     * {@link #countDown} method; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread.
     * </ul>
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * @throws InterruptedException if the current thread is interrupted
     *         while waiting
     */
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    /**
     * Causes the current thread to wait until the latch has counted down to
     * zero, unless the thread is {@linkplain Thread#interrupt interrupted},
     * or the specified waiting time elapses.
     *
     * <p>If the current count is zero then this method returns immediately
     * with the value {@code true}.
     *
     * <p>If the current count is greater than zero then the current
     * thread becomes disabled for thread scheduling purposes and lies
     * dormant until one of three things happen:
     * <ul>
     * <li>The count reaches zero due to invocations of the
     * {@link #countDown} method; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     * <li>The specified waiting time elapses.
     * </ul>
     *
     * <p>If the count reaches zero then the method returns with the
     * value {@code true}.
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * <p>If the specified waiting time elapses then the value {@code false}
     * is returned.  If the time is less than or equal to zero, the method
     * will not wait at all.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the {@code timeout} argument
     * @return {@code true} if the count reached zero and {@code false}
     *         if the waiting time elapsed before the count reached zero
     * @throws InterruptedException if the current thread is interrupted
     *         while waiting
     */
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    /**
     * Decrements the count of the latch, releasing all waiting threads if
     * the count reaches zero.
     *
     * <p>If the current count is greater than zero then it is decremented.
     * If the new count is zero then all waiting threads are re-enabled for
     * thread scheduling purposes.
     *
     * <p>If the current count equals zero then nothing happens.
     */
    public void countDown() {
        sync.releaseShared(1);
    }

    /**
     * Returns the current count.
     *
     * <p>This method is typically used for debugging and testing purposes.
     *
     * @return the current count
     */
    public long getCount() {
        return sync.getCount();
    }

    /**
     * Returns a string identifying this latch, as well as its state.
     * The state, in brackets, includes the String {@code "Count ="}
     * followed by the current count.
     *
     * @return a string identifying this latch, as well as its state
     */
    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}

 

CountDownLatch中的核心方法

首先看一下 CountDownLatch中的內部類,是一個繼承了AQS的內部類。

private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        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的構造方法 入參最後的賦值給  private volatile int state;

從例子中我們知道:CountDownLatch answer = new CountDownLatch(5);
        state = 5;

1、每一次 answer#countDown();方法調用 state會-1

2、線程調用 await()時線程發生阻塞。

 

1、await()方法:

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

其實是調用了AbstractQueuedSynchronizer #acquireSharedInterruptibly 方法

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // state狀態變量比較
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

首先判斷是否被中斷,中斷就拋出異常,的話與tryAcquireShared(arg)的返回值相比較,具體實現如下

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

如果此時 state == 0,則繼續執行,如果state >0,則進入阻塞的功能   : doAcquireSharedInterruptibly(int arg) 。

/**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED); //該函數 用於將當前線程相關的節點將入鏈表尾部
    boolean failed = true;
    try {
        for (;;) {                         //將入無限for循環
            final Node p = node.predecessor();  //獲得它的前節點
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {                  //唯一退出條件,也就是await()方法返回的條件非常重要!!
                    setHeadAndPropagate(node, r);  //該方法很關鍵具體下面分析
                    p.next = null; // help GC
                    failed = false;
                    return;  //到這裏返回
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&  
                parkAndCheckInterrupt())// 先知道線程由該函數來阻塞的的
                throw new InterruptedException();
        }
    } finally {
        if (failed)                //如果失敗或出現異常,失敗 取消該節點,以便喚醒後續節點
            cancelAcquire(node);
    }
}

addWaiter進行具體的剖析:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); //首先 new 首先創建一個新節點,並將當前線程實例封裝在內部,該節點維持一個線程引用
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail; //獲取tail節點,tail是volatile型的
    if (pred != null) {  //不爲空
        node.prev = pred;
        if (compareAndSetTail(pred, node)) { //利用CAS設置,允許失敗,後面有補救措施
            pred.next = node;
            return node;
        }
    }
    enq(node); //設置失敗,表明是第一個創建節點,或者是已經被別的線程修改過了會進入這裏
    return node;
}


再次進入 enq 進行具體的剖析:

上述方法設置未節點失敗,表明是第一個創建節點,或者是已經被別的線程修改過了會進入這裏。

仍然使用 自旋   for (;;) {  ... },直到設置成功纔可以推出方法。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize 第一個創建時爲節點爲空
            if (compareAndSetHead(new Node()))
                tail = head; //初始化時 頭尾節點相等
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) { //注意這裏,只有設置成功纔會退出,所以該節點一定會被添加
                t.next = node;         
                return t;
            }
        }
    }
}

 

繼續分析最上面的那個方法  doAcquireSharedInterruptibly(int arg) 中的內容:

for (;;) {
                final Node p = node.predecessor();
                if (p == head) {

                    /**               
                     protected int tryAcquireShared(int acquires) {
                        return (getState() == 0) ? 1 : -1;
                     }
                     */

                    int r = tryAcquireShared(arg);    //  此處仍然是比較 state狀態
                    if (r >= 0) {      // 退出   自 旋  的唯一機會
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())

                    throw new InterruptedException();
            }

看一下紅色字體部分  if ( shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt() ) :

shouldParkAfterFailedAcquire(p, node)方法:

/*
		可以看到針對前驅結點pred的狀態會進行不同的處理
		
		1.pred狀態爲SIGNAL,則返回true,表示要阻塞當前線程。
		2.pred狀態爲CANCELLED,則一直往隊列頭部回溯直到找到一個狀態不爲CANCELLED的結點,將當前節點node掛在這個結點的後面。
		3.pred的狀態爲初始化狀態,此時通過compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法將pred的狀態改爲SIGNAL。
     	
     	!!!  其實這個方法的含義很簡單,就是確保當前結點的前驅結點的狀態爲SIGNAL,
     	SIGNAL意味着線程釋放鎖後會喚醒後面阻塞的線程。畢竟,只有確保能夠被喚醒,當前線程才能放心的阻塞。
     	但是要注意只有在前驅結點已經是SIGNAL狀態後纔會執行後面的方法立即阻塞,對應上面的第一種情況。其他兩種情況則因爲返回false而重新執行一遍
		for循環。這種延遲阻塞其實也是一種高併發場景下的優化,試想我如果在重新執行循環的時候成功獲取了鎖,是不是線程阻塞喚醒的開銷就省了呢?
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 獲取前節點的狀態
        if (ws == Node.SIGNAL)    // 狀態爲SIGNAL -1  表明前節點可以運行
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        /*
         * 	如果不是則首先遍歷node鏈條找到狀態是0的節點,然後把我們新加進來的node變爲這個節點的下一個節點,然後更新這個0的節點狀態爲-1.
         */
        if (ws > 0) { 	//狀態爲CANCELLED, 如果前節點狀態大於0表明已經中斷,
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        
        } else {  //狀態爲初始化狀態(ReentrentLock語境下)
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //等於0進入這裏
        }
        return false;  // 只有前節點狀態爲Node.SIGNAL才返回真
    }

對shouldParkAfterFailedAcquire來進行一個整體的概述,首先應該明白節點的狀態,節點的狀態是爲了表明當前線程的良好度,如果當前線程被打斷了,在喚醒的過程中是不是應該忽略該線程,這個狀態標誌就是用來做這個的具體有如下幾種:

1、線程已經被取消

/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1;

2、線程需要去被喚醒
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;

3、線程正在喚醒等待條件
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;

4、線程的共享鎖應該被無條件傳播
/**
 * waitStatus value to indicate the next acquireShared should //
 * unconditionally propagate
 */
static final int PROPAGATE = -3;
 

注意:

線程狀態  大於 0 時表明該線程已近被取消,已近是無效節點,不應該被喚醒,注意:初始化鏈頭節點時頭節點狀態值爲0。

 

當該函數   shouldParkAfterFailedAcquire  返true時 線程調用 parkAndCheckInterrupt 這個阻塞自身。到這裏基本每個調用await函數都阻塞在這裏 (很關鍵哦,應爲下次喚醒,從這裏開始執行哦)

/*
     * 	在這個方法裏如果返回true,則執行parkAndCheckInterrupt()方法:
     * 	這裏首先調用LockSupport的park方法把線程寄存,然後在判斷線程的狀態,
     * 	使用interrupted和interrupt方法的區別還記得嗎?如果調用了interrupted方法,
     * 	會取消線程的中斷狀態。如果成功,則線程安全的寄存,如果寄存失敗,則返回線程的中斷狀態並取消這個中斷狀態。
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); //到這裏就完成了線程的等待,這裏的核心是調用了park方法實現的。
        return Thread.interrupted();
    }

Java中interrupt、interrupted和isInterrupted的關係與區別:

 1、interrupt()方法

調用interrupt()方法僅僅是在當前線程中打了一個停止的標記,並不是真的停止線程,需要用戶自己去監視線程的狀態爲並做處理。這一方法實際上完成的是,在線程受到阻塞時拋出一箇中斷信號,這樣線程就得以退出阻塞的狀態。更確切的說,如果線程被Object.wait, Thread.join和Thread.sleep三種方法之一阻塞,那麼,它將接收到一箇中斷異常(InterruptedException),從而提早地終結被阻塞狀態。

 public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

 

2、interrupted()方法
interrupted():測試當前線程(當前線程是指運行interrupted()方法的線程)是否已經中斷,且清除中斷狀態。

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

3、

isInterrupted()方法

isInterrupted():測試線程(調用該方法的線程)是否已經中斷,不清除中斷狀態。

public boolean isInterrupted() {
        return isInterrupted(false);
    }

interrupted和 isInterrupted兩個方法有兩個主要區別:

interrupted 是作用於當前線程,isInterrupted 是作用於調用該方法的線程對象所對應的線程。(線程對象對應的線程不一定是當前運行的線程。例如我們可以在A線程中去調用B線程對象的isInterrupted方法。)
這兩個方法最終都會調用同一個方法——isInterrupted(boolean ClearInterrupted),只不過參數 ClearInterrupted 固定爲一個是true,一個是false;下面是該方法,該方法是一個本地方法。

 private native boolean isInterrupted(boolean ClearInterrupted);


測試當前線程是否已經中斷。線程的中斷狀態由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將放回false(在第一次調用已經清除了其中斷的狀態之後,且第二次調用檢驗完中斷狀態之前,當前線程再次中斷的情況除外)。

 park(Object blocker)方法進行阻塞等待喚醒:

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

線程的掛起和恢復#

sun.misc.Unsafe  下的類 Unsafe,對線程的操作:

unpark#

  • public native void unpark(Object thread);

釋放被park創建的在一個線程上的阻塞。這個方法也可以被使用來終止一個先前調用park導致的阻塞。這個操作是不安全的,因此必須保證線程是存活的(thread has not been destroyed)。從Java代碼中判斷一個線程是否存活的是顯而易見的,但是從native代碼中這機會是不可能自動完成的。

park#

  • public native void park(boolean isAbsolute, long time);

阻塞當前線程直到一個unpark方法出現(被調用)、一個用於unpark方法已經出現過(在此park方法調用之前已經調用過)、線程被中斷或者time時間到期(也就是阻塞超時)。在time非零的情況下,如果isAbsolute爲true,time是相對於新紀元之後的毫秒,否則time表示納秒。這個方法執行時也可能不合理地返回(沒有具體原因)。併發包java.util.concurrent中的框架對線程的掛起操作被封裝在LockSupport類中,LockSupport類中有各種版本pack方法,但最終都調用了Unsafe#park()方法。

如果執行到這裏就知道該線程已經被掛起了,等着被喚醒了。

 

接下來我們就看一下可以喚醒線程的的方法吧:

2、 countDown方法:

public void countDown() {
        sync.releaseShared(1);
    }

該函數也是委託其內部類完成,具體實現如下 arg爲1:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

判斷條件tryReleaseShared:


protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {   // 狀態自旋 -1  
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc)) 此處使用CAS線程安全 -1 操作
            return nextc == 0;  // nextc  爲0時才返回真
    }
}

releaseShared方法,就是說當state減1後爲0時纔會返回爲true, 執行後面的喚醒條件,否則全部忽視,假設達到喚醒條件 具體來看如何喚醒:

private void doReleaseShared() {
    for (;;) {
        Node h = head; //獲取頭節點,
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) { // 頭結點的狀態爲Node.SIGNAL
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h); // 這裏喚醒 很重要
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;  //這裏是否有疑問明明都有這個 Node h = head爲啥還要在判斷一次?多次一舉彆着急後面有原因
    }
}

線程的共享鎖應該被無條件傳播
static final int PROPAGATE = -3;

下面執行喚醒添加

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 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); //喚醒線程
}

首先取該節點的後節點就行喚醒,如果後節點已被取消,則從最後一個開始往前找,找一個滿足添加的節點進行喚醒,

那麼看線程被喚醒後怎麼執行呢,再次看一下線程阻塞的方法:

/**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED); //該函數 用於將當前線程相關的節點將入鏈表尾部
    boolean failed = true;
    try {
        for (;;) {                         //將入無限for循環
            final Node p = node.predecessor();  //獲得它的前節點
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {                  //唯一退出條件,也就是await()方法返回的條件非常重要!!
                    setHeadAndPropagate(node, r);  //該方法很關鍵具體下面分析
                    p.next = null; // help GC
                    failed = false;
                    return;  //到這裏返回
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&  
                parkAndCheckInterrupt())// 先知道線程由該函數來阻塞的的
                throw new InterruptedException();
        }
    } finally {
        if (failed)                //如果失敗或出現異常,失敗 取消該節點,以便喚醒後續節點
            cancelAcquire(node);
    }
}

被喚醒後的線程會繼續執行代碼:

 由於線程在這裏被阻塞,喚醒後繼續執行,由於滿足條件【喚醒方法的調用條件就是  state的狀態值爲0】,則現在state的狀態值爲0,函數返回值爲1 ,大於0會進入其中我們繼續往下看 :

 for (;;) {   //將入無限for循環
            final Node p = node.predecessor();  //獲得它的前節點
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {                  //唯一退出條件,也就是await()方法返回的條件非常重要!!
                    setHeadAndPropagate(node, r);  // 該方法很關鍵
                    p.next = null; // help GC
                    failed = false;
                    return;  //到這裏返回
                }
            }

則進入setHeadAndPropagate(node, r);  方法的調用了。

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node); //這裏重新設置頭節點 (已上面  第一次釋放鎖 h== head 的重複判斷相對應)
  
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared(); //注意這裏 會進入這裏 
    }
}

再次進入 doReleaseShared()方法,就是上面喚醒線程的方法。

private void doReleaseShared() {
    	for (;;) {
            Node h = head; //獲取頭節點,
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) { // 頭結點的狀態爲Node.SIGNAL
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h); // 這裏喚醒 很重要哦
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break; 
        })
    }

CountDownLatch  的喚醒機制

  1. 先喚醒一個頭節點 線程(第一個阻塞的線程)

  2. 然後被喚醒的線程重新設置頭節點然後再次進入喚醒方法中,執行喚醒線程動作。

  3. 如此重複下去 最終所有線程都會被喚醒,其實這也是AQS共享鎖的喚醒原理,自此完成了對countDownLatch阻塞和喚醒原理的基本分析

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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