JUC併發工具十-常用的併發控制工具CountDownLatch,CyclicBarrier和Semaphore

目錄

1 CountDownLatch類使用

1.1 CountDownLatch用法

1.2 CountDownLatch原理

1.2.1 構造方法

1.2.2 countDown方法

1.2.3 await方法

1.3 CDL不可重複使用

2 CyclicBarrier類使用

2.1 CyclicBarrier用法

2.2 CyclicBarrier原理

2.2.1 構造方法

2.2.2 await方法

2.3 CyclicBarrier可重複使用

3 Semaphore類使用

3.1 Semaphore用法

3.2 Semaphore原理

3.2.1 構造方法

3.2.2 acquire方法

3.2.3 release方法


針對併發問題,JUC提供了一些工具類,來控制線程併發數。常用的有CountDownLatch、CyclicBarrier和Semaphore
CountDownLatch:數值減掉0開始執行某個步驟
CyclicBarrier:數值加到某個數值開始執行某個步驟
Semaphore:控制併發線程數

1 CountDownLatch類使用

1.1 CountDownLatch用法

假設教室裏有5個人,最後一個人走的時候班長關門,要實現這個功能
測試代碼

@Test
public void testCountDownLatch() throws InterruptedException {
    int count = 5;
    CountDownLatch countDownLatch = new CountDownLatch(count);
    for (int i = 0; i < count; i ++) {
        int finalI = i;
        new Thread(() -> {
            System.out.println(finalI + "離開");
            // 使用後把count - 1
            countDownLatch.countDown();
        }).start();
    }
    // 這裏進行阻塞
    countDownLatch.await();
    System.out.println("關門 ……");
}

輸出
0離開
1離開
4離開
3離開
2離開
關門 ……

1.2 CountDownLatch原理

CountDownLatch內部也是維護了一個基於AQS實現的一個線程同步工具不瞭解AQS的同學可以先看看筆者前面寫的

1.2.1 構造方法

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

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

是不是很眼熟,count的值實際上就是設置了AQS的state屬性

1.2.2 countDown方法

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

releaseShared是AQS的一個模版方法,筆者在講解AQS的時候已經簡單提到過,這裏不再贅述,直接看tryReleaseShared

protected boolean tryReleaseShared(int releases) {
    for (;;) {
        // 獲取當前state
        int c = getState();
        if (c == 0)
            return false;
        // 把state - 1 並CAS修改
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

閱讀源碼可知countDown方法實際上就是把資源狀態屬性state值進行減一

1.2.3 await方法

如果count > 0,await方法則阻塞,直到count減成0

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


在講解AQS文章中筆者提到過acquireShared方法,acquireSharedInterruptibly方法類似,只是多了一個線程中斷判斷,這裏不再cp
下面直接看tryAcquireShared方法

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

這裏可以看到如果state==0,則返回值是1,>0,所以這裏直接返回,不會進入doAcquireShared方法
如果state!=0,則返回值是-1,<0,進入doAcquireShared方法進行等待。
doAcquireShared方法筆者也不是很懂,只知道把當前線程添加到隊列裏等待運行,有興趣的同學可以閱讀以下源碼

1.3 CDL不可重複使用

CDL是一次性的,使用完後就失效了,不能再做併發控制
測試代碼

@Test
public void testCountDownLatch() throws InterruptedException {
    int count = 5;
    CountDownLatch countDownLatch = new CountDownLatch(count);
    for (int i = 0; i < count; i ++) {
        int finalI = i;
        new Thread(() -> {
            System.out.println(finalI + "離開");
            // 使用後把count - 1
            countDownLatch.countDown();
        }).start();
    }
    // 這裏進行阻塞
    countDownLatch.await();
    System.out.println("關門 ……");
    TimeUnit.SECONDS.sleep(1);
    for (int i = 0; i < count; i ++) {
        int finalI = i;
        new Thread(() -> {
            System.out.println(finalI + "離開");
            // 使用後把count - 1
            countDownLatch.countDown();
        }).start();
    }
    // 這裏進行阻塞
    countDownLatch.await();
    System.out.println("關門 ……");
}

輸出
0離開
1離開
3離開
2離開
4離開
關門 ……
0離開
1離開
關門 ……
2離開
3離開
4離開

2 CyclicBarrier類使用

2.1 CyclicBarrier用法

CyclicBarrier和CDL類使用差不多,只不過CDL的count是遞減的,CyclicBarrier的count是遞增的
這裏簡單實現一個小功能,集齊七顆龍珠召喚神龍

@Test
public void testCyclicBarrier() throws InterruptedException {
    int count = 7;
    CyclicBarrier cyclicBarrier = new CyclicBarrier(count, () -> System.out.println("召喚神龍 ……"));
    for (int i = 0; i < count; i ++) {
        int finalI = i;
        new Thread(() -> {
            System.out.println(finalI + "龍珠");
            try {
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

輸出:
0龍珠
2龍珠
1龍珠
4龍珠
3龍珠
5龍珠
6龍珠
召喚神龍 ……

2.2 CyclicBarrier原理

2.2.1 構造方法

// 第一個參數是屏障大小,第二個參數是達到parties後的執行方法

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

2.2.2 await方法

await方法就是一次次判斷count是否等於0,如果等於0則執行目標方法,否則循環等待。

public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    final ReentrantLock lock = this.lock;
    // 加鎖
    lock.lock();
    try {
        final Generation g = generation;

        if (g.broken)
            // 如果屏障已損壞則拋異常
            throw new BrokenBarrierException();

        if (Thread.interrupted()) {
            // 如果線程是中斷的則破壞屏障,拋異常
            breakBarrier();
            throw new InterruptedException();
        }

        // count - 1
        int index = --count;
        if (index == 0) {  // tripped
            // 如果-1後=0則運行目標方法barrierCommand
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                // 執行完目標方法後重置屏障,也是因爲有這個方法,CyclicBarrier是可以複用的
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
                if (!timed)
                    trip.await();
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    // We're about to finish waiting even if we had not
                    // been interrupted, so this interrupt is deemed to
                    // "belong" to subsequent execution.
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                throw new BrokenBarrierException();

            if (g != generation)
                return index;

            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

2.3 CyclicBarrier可重複使用

前面分析到執行完目標方法後調用nextGeneration方法重置屏障,所以是可以複用的
測試代碼

@Test
public void testCyclicBarrier() throws InterruptedException {
    int count = 7;
    CyclicBarrier cyclicBarrier = new CyclicBarrier(count, () -> System.out.println("召喚神龍 ……"));
    for (int i = 0; i < count; i ++) {
        int finalI = i;
        new Thread(() -> {
            System.out.println(finalI + "龍珠");
            try {
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();
    }

    TimeUnit.SECONDS.sleep(1);
    System.out.println("休息一年");

    for (int i = 0; i < count; i ++) {
        int finalI = i;
        new Thread(() -> {
            System.out.println(finalI + "龍珠");
            try {
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

輸出
1龍珠
0龍珠
3龍珠
2龍珠
4龍珠
5龍珠
6龍珠
召喚神龍 ……
休息一年
0龍珠
1龍珠
2龍珠
3龍珠
4龍珠
5龍珠
6龍珠
召喚神龍 ……

3 Semaphore類使用

Semaphore可以保證獲取某一個資源的最大併發數不能超過某個值,否則阻塞等待
7輛車,3個車位,只有車位中的車離開了,其他車才能繼續使用。那麼我們就可以通過Semaphore來實現

3.1 Semaphore用法

@Test
public void testSemaphore() throws InterruptedException {
    int count = 3;
    Semaphore semaphore = new Semaphore(count);
    for (int i = 0; i < 7; i ++) {
        new Thread(() -> {
            try {
                semaphore.acquire();
                System.out.println(DateUtil.datetimeToString(new Date()) + ":" + Thread.currentThread().getName() + "搶到車位");
                TimeUnit.SECONDS.sleep(3);
                System.out.println(DateUtil.datetimeToString(new Date()) + ":" + Thread.currentThread().getName() + "釋放車位");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                semaphore.release();
            }
        }).start();
    }
    TimeUnit.MINUTES.sleep(1);
}


輸出
2020-06-25 18:46:48:Thread-1搶到車位
2020-06-25 18:46:48:Thread-2搶到車位
2020-06-25 18:46:48:Thread-0搶到車位
2020-06-25 18:46:51:Thread-1釋放車位
2020-06-25 18:46:51:Thread-0釋放車位
2020-06-25 18:46:51:Thread-2釋放車位
2020-06-25 18:46:51:Thread-3搶到車位
2020-06-25 18:46:51:Thread-5搶到車位
2020-06-25 18:46:51:Thread-4搶到車位
2020-06-25 18:46:54:Thread-3釋放車位
2020-06-25 18:46:54:Thread-5釋放車位
2020-06-25 18:46:54:Thread-6搶到車位
2020-06-25 18:46:54:Thread-4釋放車位
2020-06-25 18:46:57:Thread-6釋放車位

3.2 Semaphore原理

3.2.1 構造方法

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

看到這裏我想不用我繼續往下追代碼大家也能猜出來構造方法幹啥的了,沒錯,就是設置AQS的state的值

NonfairSync(int permits) {
    super(permits);
}
Sync(int permits) {
    setState(permits);
}

3.2.2 acquire方法

acquire方法很簡單,就是把可用資源數(state變量)-1。如果剩餘資源數>=0則直接運行,否則進入線程等待隊列

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

不廢話,老知識,直接看tryAcquireShared方法在Semaphore中的實現

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        // 這裏減去可用資源數
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            // 返回剩餘資源數
            return remaining;
    }
}

3.2.3 release方法

release方法很簡單,就是把可用資源數(state變量)+1

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

tryReleaseShared在Semaphore的實現

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        // 加上可用資源數
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

 

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