併發編程學習(6)CountDownLatch、Semaphore、CyclicBarrier

CountDownLatch

countdownlatch 是一個同步工具類,它允許一個或多個線程一直等待,直到其他線程的操作執行完畢再執行,countdownlatch 提供了兩個方法,一個是 countDown,一個是 await。countdownlatch 初始化的時候需要傳入一個整數,在這個整數倒數到 0 之前,調用了 await 方法的程序都必須要等待,然後通過 countDown 來倒數

示例代碼

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        new Thread(() -> {
            System.out.println("Thread1");
            countDownLatch.countDown(); //3-1=2
            System.out.println("Thread1執行完畢");
        }).start();
        new Thread(() -> {
            System.out.println("Thread2");
            countDownLatch.countDown();//2-1=1
            System.out.println("Thread2執行完畢");
        }).start();
        new Thread(() -> {
            System.out.println("Thread3");
            countDownLatch.countDown();//1-1=0
            System.out.println("Thread3執行完畢");
        }).start();
        countDownLatch.await();
    }
輸出--------------------
Thread1
Thread2
Thread2執行完畢
Thread1執行完畢
Thread3
Thread3執行完畢
-----------不會結束

模擬高併發

public class CountDownLatchDemo extends Thread {

    static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new CountDownLatchDemo().start();
        }
        countDownLatch.countDown();
    }

    @Override
    public void run() {
        try {
            countDownLatch.await(); //阻塞  10個線程 Thread.currentThread
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //TODO 業務代碼
        System.out.println("ThreadName:" + Thread.currentThread().getName());
    }
 }
 輸出-----------------
ThreadName:Thread-2
ThreadName:Thread-4
ThreadName:Thread-3
ThreadName:Thread-8
ThreadName:Thread-0
ThreadName:Thread-1
ThreadName:Thread-6
ThreadName:Thread-5
ThreadName:Thread-9
ThreadName:Thread-7

CountDownLatch 分析

我們只需要關係兩個方法,一個是 countDown() 方法,另一個是 await() 方法,countDown() 方法每次調用都會將 state 減 1,直到state 的值爲 0;而 await 是一個阻塞方法,當 state 減爲 0 的時候,await 方法纔會返回。await 可以被多個線程調用,大家在這個時候腦子裏要有個圖:所有調用了await 方法的線程阻塞在 AQS 的阻塞隊列中,等待條件滿足(state == 0),將線程從隊列中一個個喚醒過來

await

await()

 public void await() throws InterruptedException {
     // 可中斷的共享鎖
    sync.acquireSharedInterruptibly(1);
 }

acquireSharedInterruptibly

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
    //state 如果不等於 0,說明當前線程需要加入到共享鎖隊列中
        doAcquireSharedInterruptibly(arg);
}

doAcquireSharedInterruptibly

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    //創建一個共享模式的節點添加到隊列中
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
    //  通過自選不斷判斷
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
            // 就判斷嘗試獲取鎖
                int r = tryAcquireShared(arg);
                //r>=0 表示獲取到了執行權限,這個時候因爲 state!=0,所以不會執行這段代碼
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //在阻塞線程,這也就是爲什麼會捕獲這個異常
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHeadAndPropagate
通過doReleaseShared()來解決喚醒 把全部節點改爲head頭結點

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
countDown

由於線程被 await 方法阻塞了,所以只有等到countdown 方法使得 state=0 的時候纔會被喚醒

  1. 只有當 state 減爲 0 的時候,tryReleaseShared 才返回 true, 否則只是簡單的 state = state - 1
  2. 如果 state=0, 則調用 doReleaseShared喚醒處於 await 狀態下的線程

releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
protected boolean tryReleaseShared(int releases) {
    // 遞減計數;轉換爲零時發出信號
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

doReleaseShared
共享鎖的釋放和獨佔鎖的釋放有一定的差別前面喚醒鎖的邏輯和獨佔鎖是一樣,先判斷頭結點是不是SIGNAL 狀態,如果是,則修改爲 0,並且喚醒頭結點的下一個節點
PROPAGATE: 標識爲 PROPAGATE 狀態的節點,是共享鎖模式下的節點狀態,處於這個狀態下的節點,會對線程的喚醒進行傳播

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
            // 這個 CAS 失敗的場景是:執行到這裏的時候,剛好有一個節點入隊,入隊會將這個 ws 設置爲 -1
                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
        }
        // 如果到這裏的時候,前面喚醒的線程已經佔領了 head,那麼再循環
        // 通過檢查頭節點是否改變了,如果改變了就繼續循環
        if (h == head)                   // 等於head節點就退出
            break;
    }
}

h == head:說明頭節點還沒有被剛剛用unparkSuccessor 喚醒的線程(這裏可以理解爲ThreadB)佔有,此時 break 退出循環。
h != head:頭節點被剛剛喚醒的線程(這裏可以理解爲ThreadB)佔有,那麼這裏重新進入下一輪循環,喚醒下一個節點(這裏是 ThreadB )。我們知道,等到ThreadB 被喚醒後,其實是會主動喚醒 ThreadC…

Semaphore

semaphore 也就是我們常說的信號燈,semaphore 可以控制同時訪問的線程個數,通過 acquire 獲取一個許可,如果沒有就等待,通過 release 釋放一個許可。有點類似限流的作用。叫信號燈的原因也和他的用處有關,比如某商場就 5 個停車位,每個停車位只能停一輛車,如果這個時候來了 10 輛車,必須要等前面有空的車位才能進入;比較常見的就是做限流操作;

案例

public class SemaphoreDemo {
    //限流(AQS)
    //permits; 令牌(5)
    //公平和非公平
    static class Car extends  Thread{
        private int num;
        private Semaphore semaphore;

        public Car(int num, Semaphore semaphore) {
            this.num = num;
            this.semaphore = semaphore;
        }
        public void run(){
            try {
                semaphore.acquire(); //獲得一個令牌, 如果拿不到令牌,就會阻塞
                System.out.println("第"+num+" 搶佔一個車位");
                Thread.sleep(2000);
                System.out.println("第"+num+" 開走嘍");
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
    // 可以基於公平與非公平鎖
        Semaphore semaphore=new Semaphore(5);
        for(int i=0;i<10;i++){
            new Car(i,semaphore).start();
        }
    }

}

創建 Semaphore 實例的時候,需要一個參數 permits,這個基本上可以確定是設置給 AQS 的 state 的,然後每個線程調用 acquire 的時候,執行 state = state - 1,release 的時候執行 state = state + 1,當然,acquire 的時候,如果 state = 0,說明沒有資源了,需要等待其他線程 release。

CyclicBarrier

CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續工作。CyclicBarrier 默認的構造方法是 CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用 await 方法告訴 CyclicBarrier 當前線程已經到達了屏障,然後當前線程被阻塞

案例

public class DataImportThread extends Thread{

    private CyclicBarrier cyclicBarrier;

    private String path;

    public DataImportThread(CyclicBarrier cyclicBarrier, String path) {
        this.cyclicBarrier = cyclicBarrier;
        this.path = path;
    }

    @Override
    public void run() {
        System.out.println("開始導入:"+path+" 數據");
        //TODO 可以寫業務
        try {
            cyclicBarrier.await(); //阻塞 condition.await()
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}
public class CycliBarrierDemo extends Thread{
    @Override
    public void run() {
        System.out.println("開始進行數據分析");
    }

    //循環屏障
    //可以使得一組線程達到一個同步點之前阻塞.
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier=new CyclicBarrier
                (3,new CycliBarrierDemo());
        new Thread(new DataImportThread(cyclicBarrier,"file1")).start();
        new Thread(new DataImportThread(cyclicBarrier,"file2")).start();
        new Thread(new DataImportThread(cyclicBarrier,"file3")).start();

    }
    輸出--------------------------
    開始導入:file3 數據
    開始導入:file2 數據
    開始導入:file1 數據
    開始進行數據分析
}
  1. 對於指定計數值 parties,若由於某種原因,沒有足夠的線程調用 CyclicBarrier 的 await,則所有調用 await 的線程都會被阻塞
  2. 同樣的 CyclicBarrier 也可以調用 await(timeout, unit),設置超時時間,在設定時間內,如果沒有足夠線程到達,則解除阻塞狀態,繼續工作
  3. 通過 reset 重置計數,會使得進入 await 的線程出現BrokenBarrierException
  4. 如果採用是 CyclicBarrier(int parties, RunnablebarrierAction) 構造方法,執行 barrierAction 操作的是最後一個到達的線程


CyclicBarrier 相比 CountDownLatch 來說,要簡單很多,源碼實現是基於 ReentrantLock 和 Condition 的組合使用;CyclicBarrier 和 CountDownLatch 很像,只是 CyclicBarrier 可以有不止一個柵欄,因爲它的柵欄(Barrier)可以重複使用(Cyclic)

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