CountDownLatch、CyclicBarrier和Semaphore幾個併發容器的使用
在講這幾個容器之前,本人講述一個小面試題,該題的描述:讓A、B、C三個線程同時執行,並且依次輸出A、B、C三個字母十次。面試時,想法方向是對的,但是結果是錯的。我想到的是兩種實現,分別是:wait、notifyAll配合使用和併發容器的使用。
CountDownLatch
該容器主要的作用是:多個線程之間,主線程等待其他線程執行完畢開始往下執行。代碼如下:
public static void main(String[] args) throws InterruptedException {
CountDownLatch downLatch = new CountDownLatch(3);
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
downLatch.countDown();
}).start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
downLatch.countDown();
}).start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
downLatch.countDown();
}).start();
// 等待前面三個線程執行完畢
downLatch.await();
System.out.println(Thread.currentThread().getName());
}
打印的結果如下:
Thread-0
Thread-2
Thread-1
main
CyclicBarrier
該容器的作用是:所有線程會在某個時間等待其他線程都準備完畢纔開始執行。代碼如下:
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3);
new Thread(() -> {
System.out.println("===== " + Thread.currentThread().getName() + " Start");
try {
barrier.await();
System.out.println("===== " + Thread.currentThread().getName() + " Run");
} catch (Exception e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
System.out.println("===== " + Thread.currentThread().getName() + " Start");
try {
barrier.await();
System.out.println("===== " + Thread.currentThread().getName() + " Run");
} catch (Exception e) {
e.printStackTrace();
}
}, "B").start();
new Thread(() -> {
System.out.println("===== " + Thread.currentThread().getName() + " Start");
try {
barrier.await();
System.out.println("===== " + Thread.currentThread().getName() + " Run");
} catch (Exception e) {
e.printStackTrace();
}
}, "C").start();
}
結果如下:
===== A Start
===== C Start
===== C Run
===== A Run
===== B Run
統一執行完start就等待其他線程到start完畢後開始一起執行run
Semaphore
該容器的作用是: 限制同時執行的線程數量,所以可以用於限流作用;代碼如下:
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(1);
CyclicBarrier barrier = new CyclicBarrier(3);
new Thread(() -> {
try {
System.out.println(" ===" + Thread.currentThread().getName());
barrier.await();
// 獲取令牌
semaphore.acquire();
System.out.println(" ===" + Thread.currentThread().getName() + " Run");
Thread.sleep(3000);
// 返還令牌
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
System.out.println(" ===" + Thread.currentThread().getName());
barrier.await();
// 獲取令牌
semaphore.acquire();
System.out.println(" ===" + Thread.currentThread().getName() + " Run");
Thread.sleep(3000);
// 返還令牌
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
System.out.println(" ===" + Thread.currentThread().getName());
barrier.await();
// 獲取令牌
semaphore.acquire();
System.out.println(" ===" + Thread.currentThread().getName() + " Run");
Thread.sleep(3000);
// 返還令牌
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
結果如下:
===Thread-0
===Thread-1
===Thread-2
===Thread-2 Run
===Thread-1 Run
===Thread-0 Run
2 Run 、1 Run、0 Run 之間會有間隔,間隔時間爲3S,等待獲取執行權的線程歸還令牌;如果去掉semaphore.acquire();
和semaphore.release();
這兩句,三個Run之間瞬間執行完畢
最後我們再次回到開始的面試題,這題有兩個關鍵點是同時執行和循序打印ABC三個字母;考察的是對多線程之間,線程控制的掌握程度。
同時執行 我們可以使用wait 和 notifyAll配合使用解決 或 CyclicBarrier容器解決;方法2上面代碼示例代碼已經給出;1方法代碼如下:
private static volatile int flag = 0;
private static final Object object = new Object();
private static void test() throws InterruptedException {
new Thread(() -> {
try {
synchronized (object) {
flag++;
object.wait();
System.out.println(" = A = ");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
synchronized (object) {
flag++;
object.wait();
System.out.println(" = B = ");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
synchronized (object) {
flag++;
object.wait();
System.out.println(" = C = ");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 三個線程都啓動完畢
while (flag < 3) {
}
System.out.println(" ============= ");
Thread.sleep(5000);
synchronized (object) {
object.notifyAll();
}
}
效果:打印完=============
暫停了5秒,然後打印ABC
依次循環打印ABC
-
普通方法是通過定義一個可見變量分別取餘判定打印,代碼如下:
//TODO 可見性 或者使用 int 類型 爲什麼? // 好像是基本類型變量存儲在棧中,然後呢?求解 private static volatile Integer ctr = 1; private static void test2() { new Thread(() -> print(), "A").start(); new Thread(() -> print(), "B").start(); new Thread(() -> print(), "C").start(); } private static void print() { int i = 1; String name = Thread.currentThread().getName(); char ch = name.charAt(0); while (i <= 10) { if ((ctr % 3) == (ch + 1 - 'A') || ((ctr % 3 == 0) && ch == 'C')) { System.out.println("i = " + i + "--" + ch); i++; ctr++; } } }
效果如下:
i = 1--A i = 1--B i = 1--C i = 2--A i = 2--B i = 2--C i = 3--A i = 3--B i = 3--C i = 4--A i = 4--B i = 4--C ....
-
用Semaphore容器,思路是定義三個該容器,兩兩交叉互相釋放(返還)對方的令牌;代碼如下:
private static Semaphore sepA = new Semaphore(1); private static Semaphore sepB = new Semaphore(0); private static Semaphore sepC = new Semaphore(0); private static void test2() { // A線程 new Thread(() -> { try { for (int i = 0; i < 10; i++) { // 取A的令牌 sepA.acquire(); System.out.println("i = " + (i+1) + "--A"); // 返還B的令牌 sepB.release(); } } catch (Exception e) { e.printStackTrace(); } }).start(); // B線程 new Thread(() -> { try { for (int i = 0; i < 10; i++) { // 獲取B的令牌,A執行完纔有B的令牌 sepB.acquire(); System.out.println("i = " + (i+1) + "--B"); // 釋放C的令牌 sepC.release(); } } catch (Exception e) { e.printStackTrace(); } }).start(); new Thread(() -> { try { for (int i = 0; i < 10; i++) { // 獲取C的令牌,B執行完纔有C的令牌 sepC.acquire(); System.out.println("i = " + (i+1) + "--C"); // 釋放A的令牌,進入下一次循環 sepA.release(); } } catch (Exception e) { e.printStackTrace(); } }).start(); }
效果如下:
i = 1--A i = 1--B i = 1--C i = 2--A i = 2--B i = 2--C i = 3--A i = 3--B i = 3--C i = 4--A i = 4--B i = 4--C ....
CountDownLatch源碼分析
刪掉註釋,發現該類的代碼很少,如下:
package java.util.concurrent;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class CountDownLatch {
/**
* Synchronization control For CountDownLatch.
* Uses AQS state to represent count.
* 用於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;
}
}
}
private final Sync sync;
/**
* CountDownLatch 的代碼如下
*/
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void countDown() {
sync.releaseShared(1);
}
public long getCount() {
return sync.getCount();
}
}
發現CountDownLatch
的代碼沒啥看的,只是包裝了一個API供調用,在這裏很疑惑爲什麼不直接暴露Sync
而要再次包裝一層 ,希望有人解答一下。
所以我們看一下Sync
類做了什麼,第一眼看到的是繼承了AbstractQueuedSynchronizer
類,該類就是同步容器AQS
。所以說CountDownLatch
的其實是基於AQS
。對於AQS鑑於篇幅過長,暫不講述。
CyclicBarrier源碼分析
我們看一下幾個重要的方法代碼
字段和構造方法
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/** The number of parties */
private final int parties;
/* The command to run when tripped */
private final Runnable barrierCommand;
/**
* Number of parties still waiting. Counts down from parties to 0
* on each generation. It is reset to parties on each new
* generation or when broken.
*/
private int count;
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
parties
等待線程羣組數,不知道這個翻譯對不對
barrierCommand
count
構造方法沒什麼說的,初始化配置參數
await方法
代碼如下:
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
// 默認是false 如果爲true說明打破屏障了,線程已經運行了
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
// 當前線程(用戶線程)是否被打斷
breakBarrier();
throw new InterruptedException();
}
// 每調用一次改方法,count 自減1
int index = --count;
if (index == 0) {
// tripped 如果count 爲0 線程開始執行
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 喚醒所有的等待線程並且重置容器配置
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();
}
}
/**
* 打破屏障,singnal所有線程,運行
*/
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
兩個public
方法都最終調用的是private
方法,具體解釋看代碼片段註釋。從源碼中我們可以發現CyclicBarrier
的實現原理是基於ReentrantLock
和 Condition
的實現,這兩玩意是什麼,自行百度。其實和 對象的await
和 notify/notifyAll
方法類似的功能,而ReentrantLock
和 Condition
又有一部分是基於 AQS
實現的。此時是不是會發現AQS
在併發控制和多線程的地位很高,有時間該去分析一下它的源碼.
Semaphore的源碼淺析
內部類
// 又是基於AQS的實現
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
// 非公平的獲取許可認證
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || // 就因爲沒有剩餘判斷就非公平,爲啥?求解釋
compareAndSetState(available, remaining))
return remaining;
}
}
// 釋放許可認證
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;
}
}
// 減少許可認證
final void reducePermits(int reductions) {
for (;;) {
int current = getState();
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
if (compareAndSetState(current, next))
return;
}
}
// 清空許可認證
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
}
/**
* NonFair version
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
/**
* Fair version
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
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;
}
}
}
發現這些內部類又是基於AQS
的實現。
構造方法
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
構造方法比較簡單,就兩個。通過構造方法發現默認是非公平實現,要公平實現一定要構造方法中指明。
acquire和release方法
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
public void acquireUninterruptibly(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireShared(permits);
}
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
public boolean tryAcquire(int permits) {
if (permits < 0) throw new IllegalArgumentException();
return sync.nonfairTryAcquireShared(permits) >= 0;
}
public boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}
public void release() {
sync.releaseShared(1);
}
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
每一個acquire
和release
都有兩個,一個是默認獲取/釋放一個令牌和獲取/釋放自定義數量令牌。
總結
最後我們發現其實我們可以基與ReentrantLock
和Condition
再實現等待一起執行,代碼如下:
public static void main(String[] args) throws InterruptedException {
thread();
System.out.println("before sleep");
Thread.sleep(20000);
System.out.println("after sleep");
thread();
}
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static volatile Integer count = 2;
private static void thread() {
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " Start");
if (count > 1) {
count--;
condition.await();
} else {
condition.signalAll();
}
System.out.println(Thread.currentThread().getName() + " Run");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
}
效果如下:
before sleep
Thread-0 Start
after sleep
Thread-1 Start
Thread-1 Run
Thread-0 Run
線程0 會等待 線程1 Start 之後開始一起執行
發現AQS
在多線程和併發控制地位非常重要,離不開它的身影。
本人沒有仔細觀看AQS
的源碼,所以沒有解釋和AQS
相關的方法功能以及作用,後面有時間看了AQS
源碼後再來補全吧。還有用詞不準確的地方希望大家指出。只是通過這麼一個面試題,學習了一下線程之間控制的方法方式。