系统掌握并发编程系列(二)详解Thread类的主要属性和方法
系统掌握并发编程系列(四)详细分析传统并发协同方式(synchronized与wait() notify())
系统掌握并发编程系列(五)讲透传统并发协同方式伪唤醒与加锁失效问题
系统掌握并发编程系列(六)详细讲解并发协同利器CountDownLatch
上一篇文章讲解了CountDownLatch的用法,本文我们来讲解另一个并发协同利器CyclicBarrier。回顾下CountDownLatch的用法,倒计时锁存器减到0后,所有等待状态的线程将被释放。如果我们要一组或多组线程重复执行“等待-释放”这一个动作,比如高并发测试接口场景中,需要循环压测接口,那该怎么办?在CountDownLatch官方说明中,给出了答案。
...
* <p>A {@code CountDownLatch} is initialized with a given <em>count</em>.
* The {@link #await await} methods block until the current count reaches
* zero due to invocations of the {@link #countDown} method, after which
* all waiting threads are released and any subsequent invocations of
* {@link #await await} return immediately. This is a one-shot phenomenon
* -- the count cannot be reset. If you need a version that resets the
* count, consider using a {@link CyclicBarrier}.
*
..
* <p>Another typical usage would be to divide a problem into N parts,
* describe each part with a Runnable that executes that portion and
* counts down on the latch, and queue all the Runnables to an
* Executor. When all sub-parts are complete, the coordinating thread
* will be able to pass through await. (When threads must repeatedly
* count down in this way, instead use a {@link CyclicBarrier}.)
*
...
CyclicBarrier
上述CountDownLatch官方说明,我们发现CountDownLatch有一个重要的特性,就是倒计时锁存器不能重复利用,就是一次性使用的,减到0后将不能重置,如果要重复使用,请使用CyclicBarrier。Cyclic的字面意思是”循环的“,Barrier的字面意思是“篱栅“,”栅栏“的意思,就像一个院子里的栅栏,也可以理解为屏障。所以CyclicBarrier在java中的专业名称是循环屏障,从字面意思上理解就是可以循环使用的屏障。
/**
* A synchronization aid that allows a set of threads to all wait for
* each other to reach a common barrier point. CyclicBarriers are
* useful in programs involving a fixed sized party of threads that
* must occasionally wait for each other. The barrier is called
* <em>cyclic</em> because it can be re-used after the waiting threads
* are released.
*
* <p>A {@code CyclicBarrier} supports an optional {@link Runnable} command
* that is run once per barrier point, after the last thread in the party
* arrives, but before any threads are released.
* This <em>barrier action</em> is useful
* for updating shared-state before any of the parties continue.
*
...
public class CyclicBarrier {
...
}
CyclicBarrier是一个多线程协同工具,它允许一组线程相互等待彼此,以达到一个公共的屏障点,当所有的线程都到达这个屏障点后再一起继续执行。循环屏障通常用在需要一组固定大小的线程必须相互等待的程序中。之所以叫循环,是因为屏障在释放等待线程后可以被重复使用。下面我们来分析下CyclicBarrier有哪些方法,弄清楚怎么用它。
1、构造函数CyclicBarrier(int parties, Runnable barrierAction)
/**
* Creates a new {@code CyclicBarrier} that will trip when the
* given number of parties (threads) are waiting upon it, and which
* will execute the given barrier action when the barrier is tripped,
* performed by the last thread entering the barrier.
*
* @param parties the number of threads that must invoke {@link #await}
* before the barrier is tripped
* @param barrierAction the command to execute when the barrier is
* tripped, or {@code null} if there is no action
* @throws IllegalArgumentException if {@code parties} is less than 1
*/
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
该构造函数通过传入一个参与线程的数量,一个线程执行的任务(或者叫屏障动作,如果没有也可以传null),创建一个循环屏障,循环屏障将被打开当指定数量的线程都达到屏障点后,最后一个达到屏障点的线程将执行传入的任务(屏障动作),屏障动作只执行一次。
/**
* Creates a new {@code CyclicBarrier} that will trip when the
* given number of parties (threads) are waiting upon it, and
* does not perform a predefined action when the barrier is tripped.
*
* @param parties the number of threads that must invoke {@link #await}
* before the barrier is tripped
* @throws IllegalArgumentException if {@code parties} is less than 1
*/
public CyclicBarrier(int parties) {
this(parties, null);
}
通过该构造函数创建的循环屏障,只指定了参与线程的数量,没有指定屏障动作。知道了怎么创建屏障后,下面来分析下怎么使用这个屏障。
2、await()方法
/**
* Waits until all {@linkplain #getParties parties} have invoked
* {@code await} on this barrier.
*
* <p>If the current thread is not the last to arrive then it is
* disabled for thread scheduling purposes and lies dormant until
* one of the following things happens:
* <ul>
* <li>The last thread arrives; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
* the current thread; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
* one of the other waiting threads; or
* <li>Some other thread times out while waiting for barrier; or
* <li>Some other thread invokes {@link #reset} on this barrier.
* </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.
*
* <p>If the barrier is {@link #reset} while any thread is waiting,
* or if the barrier {@linkplain #isBroken is broken} when
* {@code await} is invoked, or while any thread is waiting, then
* {@link BrokenBarrierException} is thrown.
*
* <p>If any thread is {@linkplain Thread#interrupt interrupted} while waiting,
* then all other waiting threads will throw
* {@link BrokenBarrierException} and the barrier is placed in the broken
* state.
*
* <p>If the current thread is the last thread to arrive, and a
* non-null barrier action was supplied in the constructor, then the
* current thread runs the action before allowing the other threads to
* continue.
* If an exception occurs during the barrier action then that exception
* will be propagated in the current thread and the barrier is placed in
* the broken state.
*
* @return the arrival index of the current thread, where index
* {@code getParties() - 1} indicates the first
* to arrive and zero indicates the last to arrive
* @throws InterruptedException if the current thread was interrupted
* while waiting
* @throws BrokenBarrierException if <em>another</em> thread was
* interrupted or timed out while the current thread was
* waiting, or the barrier was reset, or the barrier was
* broken when {@code await} was called, or the barrier
* action (if present) failed due to an exception
*/
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
调用该方法将会使当前线程进入等待状态,表明当前线程已到屏障点。如果当前线程不是最后一个到达屏障点的线程,那么该线程将不会被调度运行并进入等待状态,直到下列的任一条件发生:
(1)最后一个参与线程到达屏障点。
(2)其他线程中断当前线程。
(3)其他线程中断处于等待状态的线程中的一个。
(4)其他线程在屏障点等待超时。
(5)其他线程调用该屏障的reset方法。
异常抛出情况:
(1)进入该方法时,如果当前线程的中断状态被设置,或者在等待的过程被中断,那么将会抛出中断异常。
(2)如果屏障点被重置,或者屏障点被破坏,那么将会抛出破坏屏障异常。
(3)如果任意一个处于等待状态的线程被中断,那么所有等待状态的线程将抛出破坏屏障异常,且屏障将处于破坏状态。
如果当前线程是最后一个到达屏障点的线程并且屏障动作不为空,那么该线程将在释放所有等待状态的线程之前执行屏障动作,如果屏障动作执行过程中发生异常,那么异常将在当前线程传播并且屏障将处于破坏状态。
调用该方法将返回一个int类型的值,表示当前线程到达屏障点的索引号,第一个达到屏障点的线程的索引号数是parties-1(参与线程数减1),最后一个达到屏障点的线程的索引号是0。
/**
* Waits until all {@linkplain #getParties parties} have invoked
* {@code await} on this barrier, or the specified waiting time elapses.
*
...
* <p>If the specified waiting time elapses then {@link TimeoutException}
* is thrown. If the time is less than or equal to zero, the
* method will not wait at all.
*
...
* @param timeout the time to wait for the barrier
* @param unit the time unit of the timeout parameter
...
* @throws TimeoutException if the specified timeout elapses.
* In this case the barrier will be broken.
* @throws BrokenBarrierException if <em>another</em> thread was
* interrupted or timed out while the current thread was
* waiting, or the barrier was reset, or the barrier was broken
* when {@code await} was called, or the barrier action (if
* present) failed due to an exception
*/
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
调用该方法将会使当前线程进入等待状态,可以指定等待时长,等待超时后将抛出超时异常。其他的和无参的await()一样。
3、int getNumberWaiting() 方法
/**
* Returns the number of parties currently waiting at the barrier.
* This method is primarily useful for debugging and assertions.
*
* @return the number of parties currently blocked in {@link #await}
*/
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;
} finally {
lock.unlock();
}
}
调用该方法可以返回当前在屏障点等待的线程数量,通常用于调试和断言。
4、int getParties()方法
/**
* Returns the number of parties required to trip this barrier.
*
* @return the number of parties required to trip this barrier
*/
public int getParties() {
return parties;
}
调用该方法返回需要到屏障点等待的线程数量,也就是创建屏障时指定的线程数量。
5、void reset()方法
/**
* Resets the barrier to its initial state. If any parties are
* currently waiting at the barrier, they will return with a
* {@link BrokenBarrierException}. Note that resets <em>after</em>
* a breakage has occurred for other reasons can be complicated to
* carry out; threads need to re-synchronize in some other way,
* and choose one to perform the reset. It may be preferable to
* instead create a new barrier for subsequent use.
*/
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
调用该方法将重置屏障到初始状态,当前在屏障等待的线程将抛出破坏屏障异常。
6、boolean isBroken()
/**
* Queries if this barrier is in a broken state.
*
* @return {@code true} if one or more parties broke out of this
* barrier due to interruption or timeout since
* construction or the last reset, or a barrier action
* failed due to an exception; {@code false} otherwise.
*/
public boolean isBroken() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return generation.broken;
} finally {
lock.unlock();
}
}
该方法用于查询屏障是否处于破坏状态。如果一个或者多个参与线程因为被中断、等待超时、重置屏障或者屏障动作执行异常,那么该方法将返回true,表明屏障被破坏;否则将返回false。
应用举例
了解了创建屏障对象和屏障的常用方法后,我们来看怎么使用。还是用上一篇多个线程同时调用接口的例子,将通知线程开始同时调用的实现由CountDownLatch修改为CyclicBarrier,如下:
public class ConcurrentStartDemo {
public static void main(String[] args) throws InterruptedException {
int threads = 5;
// 开始信号
//CountDownLatch startSignal = new CountDownLatch(1);
CyclicBarrier startSignal = new CyclicBarrier(threads);
// 准备完成信号
CountDownLatch readySignal = new CountDownLatch(threads);
// 调用完成信号
CountDownLatch doneSignal = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
new Thread(() -> {
try {
//准备完成
readySignal.countDown();
System.out.println(Thread.currentThread().getName() + "," + " 已准备就绪,等待开始信号...");
// 等待主线程发出开始信号
startSignal.await();
System.out.println("时间戳" + System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 开始调用接口...");
// 模拟调用耗时
Thread.sleep(1000);
doneSignal.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
//等待所有子线程准备完成
System.out.println("主线程等待所有子线程准备完成...");
readySignal.await();
System.out.println("主线程发出开始信号...");
// 所有子线程开始调用接口
//startSignal.countDown();
// 等待所有子线程完成任务
doneSignal.await();
System.out.println("所有子线程已完成接口调用...");
}
}
运行结果:
主线程等待所有子线程准备完成...
Thread-1, 已准备就绪,等待开始信号...
Thread-2, 已准备就绪,等待开始信号...
Thread-0, 已准备就绪,等待开始信号...
Thread-3, 已准备就绪,等待开始信号...
Thread-4, 已准备就绪,等待开始信号...
主线程发出开始信号...
时间戳1746952561224,Thread-4 开始调用接口...
时间戳1746952561224,Thread-1 开始调用接口...
时间戳1746952561224,Thread-2 开始调用接口...
时间戳1746952561224,Thread-3 开始调用接口...
时间戳1746952561224,Thread-0 开始调用接口...
所有子线程已完成接口调用...
从运行结果可以看出,效果是一样的,和CountDownLatch的使用区别主要是线程调用await()方法进入等待后,无需对计数器减1操作,等最后一个线程到达屏障点后所有线程自动释放。
到这里可能会问,既然是循环屏障,上面的例子只用了一次而已,那要怎么让它循环使用。还是用上面的例子,修改下需求,需要两轮压测,先用CountDownLatch来实现。
public class ConcurrentStartDemo {
public static void main(String[] args) throws InterruptedException {
int threads = 5;
// 开始信号
CountDownLatch startSignal = new CountDownLatch(1);
System.out.println("第一轮压测开始...");
doConcurrentTest(threads, startSignal);
System.out.println("第一轮压测完成...");
Thread.sleep(5000);
System.out.println("第二轮压测开始...");
doConcurrentTest(threads, startSignal);
System.out.println("第二轮压测完成...");
}
private static void doConcurrentTest(int threads, CountDownLatch startSignal) throws InterruptedException {
// 准备完成信号
CountDownLatch readySignal = new CountDownLatch(threads);
// 调用完成信号
CountDownLatch doneSignal = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
new Thread(() -> {
try {
//准备完成
readySignal.countDown();
System.out.println(Thread.currentThread().getName() + "," + " 已准备就绪,等待开始信号...");
// 等待主线程发出开始信号
startSignal.await();
System.out.println("时间戳" + System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 开始调用接口...");
// 模拟调用耗时
Thread.sleep(1000);
doneSignal.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
//等待所有子线程准备完成
System.out.println("主线程等待所有子线程准备完成...");
readySignal.await();
System.out.println("主线程发出开始信号...");
// 所有子线程开始调用接口
startSignal.countDown();
// 等待所有子线程完成任务
doneSignal.await();
System.out.println("所有子线程已完成接口调用...");
}
}
看看运行结果:
第一轮压测开始...
主线程等待所有子线程准备完成...
Thread-0, 已准备就绪,等待开始信号...
Thread-1, 已准备就绪,等待开始信号...
Thread-2, 已准备就绪,等待开始信号...
Thread-3, 已准备就绪,等待开始信号...
Thread-4, 已准备就绪,等待开始信号...
主线程发出开始信号...
时间戳1746953175160,Thread-1 开始调用接口...
时间戳1746953175160,Thread-4 开始调用接口...
时间戳1746953175160,Thread-0 开始调用接口...
时间戳1746953175160,Thread-2 开始调用接口...
时间戳1746953175160,Thread-3 开始调用接口...
所有子线程已完成接口调用...
第一轮压测完成...
第二轮压测开始...
Thread-6, 已准备就绪,等待开始信号...
Thread-5, 已准备就绪,等待开始信号...
时间戳1746953181192,Thread-6 开始调用接口...
时间戳1746953181193,Thread-5 开始调用接口...
主线程等待所有子线程准备完成...
Thread-7, 已准备就绪,等待开始信号...
Thread-8, 已准备就绪,等待开始信号...
时间戳1746953181194,Thread-7 开始调用接口...
主线程发出开始信号...
时间戳1746953181194,Thread-8 开始调用接口...
Thread-9, 已准备就绪,等待开始信号...
时间戳1746953181195,Thread-9 开始调用接口...
所有子线程已完成接口调用...
第二轮压测完成...
从运行结果可以看出,第一轮压测的时候各个线程还是很听指挥的,到了第二轮压测时就各干各的了,说明用过的CountDownLatch已经没法重复使用来协同控制线程的执行了,将开始信号修改为CyclicBarrier来实现,指定屏障动作输出"所有线程准备完成…"。
public class ConcurrentStartDemo {
public static void main(String[] args) throws InterruptedException {
int threads = 5;
// 开始信号
CyclicBarrier startSignal = new CyclicBarrier(threads, ()-> System.out.println("所有线程准备完成..."));
System.out.println("第一轮压测开始...");
doConcurrentTest(threads, startSignal);
Thread.sleep(5000);
System.out.println("第二轮压测开始...");
doConcurrentTest(threads, startSignal);
}
private static void doConcurrentTest(int threads, CyclicBarrier startSignal) throws InterruptedException {
// 准备完成信号
CountDownLatch readySignal = new CountDownLatch(threads);
// 调用完成信号
CountDownLatch doneSignal = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
new Thread(() -> {
try {
//准备完成
readySignal.countDown();
System.out.println(Thread.currentThread().getName() + "," + " 已准备就绪,等待开始信号...");
// 等待主线程发出开始信号
startSignal.await();
System.out.println("时间戳" + System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 开始调用接口...");
// 模拟调用耗时
Thread.sleep(1000);
doneSignal.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
//等待所有子线程准备完成
System.out.println("主线程等待所有子线程准备完成...");
readySignal.await();
System.out.println("主线程发出开始信号...");
// 所有子线程开始调用接口
//startSignal.countDown();
// 等待所有子线程完成任务
doneSignal.await();
System.out.println("所有子线程已完成接口调用...");
}
}
运行结果:
第一轮压测开始...
主线程等待所有子线程准备完成...
Thread-0, 已准备就绪,等待开始信号...
Thread-1, 已准备就绪,等待开始信号...
Thread-2, 已准备就绪,等待开始信号...
Thread-3, 已准备就绪,等待开始信号...
Thread-4, 已准备就绪,等待开始信号...
所有线程准备完成...
主线程发出开始信号...
时间戳1746953686791,Thread-4 开始调用接口...
时间戳1746953686791,Thread-1 开始调用接口...
时间戳1746953686791,Thread-3 开始调用接口...
时间戳1746953686791,Thread-2 开始调用接口...
时间戳1746953686791,Thread-0 开始调用接口...
所有子线程已完成接口调用...
第一轮压测完成...
第二轮压测开始...
主线程等待所有子线程准备完成...
Thread-5, 已准备就绪,等待开始信号...
Thread-6, 已准备就绪,等待开始信号...
Thread-7, 已准备就绪,等待开始信号...
Thread-8, 已准备就绪,等待开始信号...
Thread-9, 已准备就绪,等待开始信号...
所有线程准备完成...
主线程发出开始信号...
时间戳1746953692812,Thread-9 开始调用接口...
时间戳1746953692812,Thread-5 开始调用接口...
时间戳1746953692812,Thread-6 开始调用接口...
时间戳1746953692812,Thread-7 开始调用接口...
时间戳1746953692812,Thread-8 开始调用接口...
所有子线程已完成接口调用...
第二轮压测完成...
从运行结果可以看出,两轮压测线程都是同时发起的,说明第一次使用CyclicBarrier后,第二次还可以使用。
总结
本文详细讲解了循环屏障CyclicBarrier的主要方法,通过“同时对接口发起多次压测”这个例子,和用CountDownLatch实现对比,说明了循环屏障的使用。使用CyclicBarrier需要注意的是,指定了参与线程数创建屏障后,一定要确保有足够的参与线程,否则会一直阻塞在屏障处。根据上面的例子,总结出CyclicBarrier的使用场景:多个线程等待同时执行,或者多个线程多次(循环)等待同时执行。
如果文章对您有帮助,不妨“帧栈”一下,关注“帧栈”公众号,第一时间获取推送文章,您的肯定将是我写作的动力!