并发编程原理与实战(四)经典并发协同方式synchronized与wait+notify详解
并发编程原理与实战(五)经典并发协同方式伪唤醒与加锁失效原理揭秘
前面分析了通过synchronized和wait(),notify(),notifyAll()方法实现传统多线程并发协同的方式,并通过“两个线程顺序交替输出数字”,“生产者-消费者模型处理任务”等例子来说明。这两个例子都是基于无限循环条件判断,满足条件的线程就进入运行状态,不满足条件就进入等待状态,运行完成的线程触发修改条件并唤醒等待状态的线程执行,这是多线程协同运行的场景之一。
现在我们有个场景:有一个订单查询详情接口,执行的业务逻辑比较多,先查询订单主要信息,再查订单商品信息,接着查询卖家信息,再查订单买家信息,最后将这些信息返回给前端。要查这么多信息,如果用单个线程串行执行,耗时比较久;现在要用多线程去改造,每部分信息用一个子线程去查询,这样每部分信息的查询就可以并行执行,最后主线程负责将查询结果返回前端。相比单个主线程串行执行,多个子线程并行查询会不会加快整个接口的响应速度,接下来论证下。
先分析下这个场景,用synchronized和wait(),notify(),notifyAll()方法能否实现?这个场景中,主线程负责接收前端请求、创建子线程执行信息查询、返回查询结果到前端;子线程负责查询指定信息。主子线程运行的条件都是一样,就是有前端请求过来,请求过来后需要主线程等待子线程执行完成,子线程并不需要等待主线程,子线程之间也不需要相互等待或者唤醒。很明显,用传统并发协同方法无法实现这样的线程协同场景,因为这里不需要无限循环判断条件是否满足,不需要线程之间的相互唤醒。那么jdk中还提供了哪些并发协同工具可以用于这种场景?倒计时锁存器是其中一种。
倒计时锁存器CountDownLatch
CountDownLatch 是 Java 并发包(java.util.concurrent)中一个重要的线程并发协同工具类,用于控制多个线程的执行顺序,它允许一个或多个线程等待其他线程完成一系列操作后再继续执行,这个就适合用于对订单详情查询接口的改造。下面看看官方说明。
/**
* A synchronization aid that allows one or more threads to wait until
* a set of operations being performed in other threads completes.
*
* <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>A {@code CountDownLatch} is a versatile synchronization tool
* and can be used for a number of purposes. A
* {@code CountDownLatch} initialized with a count of one serves as a
* simple on/off latch, or gate: all threads invoking {@link #await await}
* wait at the gate until it is opened by a thread invoking {@link
* #countDown}. A {@code CountDownLatch} initialized to <em>N</em>
* can be used to make one thread wait until <em>N</em> threads have
* completed some action, or some action has been completed N times.
*
* <p>A useful property of a {@code CountDownLatch} is that it
* doesn't require that threads calling {@code countDown} wait for
* the count to reach zero before proceeding, it simply prevents any
* thread from proceeding past an {@link #await await} until all
* threads could pass.
*
....
*
* <p>Memory consistency effects: Until the count reaches
* zero, actions in a thread prior to calling
* {@code countDown()}
* <a href="package-summary.html#MemoryVisibility"><i>happen-before</i></a>
* actions following a successful return from a corresponding
* {@code await()} in another thread.
*
* @since 1.5
* @author Doug Lea
*/
public class CountDownLatch {
...
}
通过给定一个数字初始化CountDownLatch对象,或者叫初始化倒计时锁存器,线程调用倒计时锁存器的 countDown() 方法减少计数,线程调用倒计时锁存器的await()方法会阻塞,直到计数减到零才会继续运行。这个倒计时锁存器就像一个门闩,没有减到零时,所有调用了await()方法的线程都集中门里等待,只有减到零后才会开门让线程继续执行。
这里需要注意的是,调用 countDown() 方法的线程在运行前并不需要等待计数减到零。这和上面分析的订单详情查询接口中,子线程不需要等待主线程场景吻合。
倒计时锁存器方法分析
倒计时锁存器的成员方法并不多,我们来逐一分析下。
1、构造函数CountDownLatch(int count)
/**
* 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);
}
该构造函数通过指定一个整型值创建一个CountDownLatch对象,这个整型值就是countDown() 方法被调用的次数。
2、countDown()方法
/**
* 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);
}
调用该方法将会对计数器减1,如果减到0将会释放所有等待状态的线程,这些线程将会被调度运行。如果当前计数器等于0,调用该方法将什么都不做。
3、await()方法
/**
* 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);
}
调用该方法将会导致线程进入等待状态直到计数器减到0,除非线程被中断等待。如果计数器等于0,调用该方法会立即返回。
4、boolean await(long timeout, TimeUnit unit)方法
/**
* 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.
*
...
*/
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
该方法和上一个方法的区别是可以指定等待时间和有返回值。调用该方法将会导致线程进入等待状态直到计数器减到0,除非线程被中断等待或者指定的等待时间用完。如果计数器已经减到0了,那么将返回true;如果在等待时间用完前计数器还没有减到0那么将返false。
5、long getCount()方法
/**
* 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();
}
返回当前计数器的数值,通常用于调试和测试。
应用举例
分析完CountDownLatch的构造函数和成员方法后,我们把这个倒计时锁存器应用到订单详情查询接口改造中,相应的就是主线程调用await()方法等待子线程执行完成,子线程完成查询后调用countDown() 方法对计数减1。
public class CountDownLatchDemo {
public static void main(String[] args) throws Exception{
long start = System.currentTimeMillis();
ConcurrentHashMap<String,String> retMap = new ConcurrentHashMap();
CountDownLatch countDownLatch = new CountDownLatch(4);
//订单查询
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"订单查询开始...");
//模拟查询耗时
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
retMap.put("orderInfo","7897562412");
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName()+"订单查询完成...");
},"t1").start();
//商品查询
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"商品查询开始...");
//模拟查询耗时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
retMap.put("goodsInfo","香蕉");
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName()+"商品查询完成...");
},"t2").start();
//卖家信息查询
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"卖家信息查询开始...");
//模拟查询耗时
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
retMap.put("storeInfo","鲜果铺");
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName()+"卖家信息查询完成...");
},"t3").start();
//买家信息查询
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"买家信息查询开始...");
//模拟查询耗时
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
retMap.put("buyerInfo","帧栈");
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName()+"买家信息查询完成...");
},"t4").start();
System.out.println("主线程等待中...");
countDownLatch.await();
System.out.println("订单详情查询完成,"+ JSON.toJSONString(retMap));
long end = System.currentTimeMillis();
System.out.println("耗时"+ (end-start)+"毫秒");
}
}
运行结果:
主线程等待中...
t1订单查询开始...
t2商品查询开始...
t3卖家信息查询开始...
t4买家信息查询开始...
t3卖家信息查询完成...
t4买家信息查询完成...
t2商品查询完成...
t1订单查询完成...
订单详情查询完成,{"buyerInfo":"帧栈","orderInfo":"7897562412","storeInfo":"鲜果铺","goodsInfo":"香蕉"}
耗时2124毫秒
如果是单线程串行执行,那么总耗时至少大于等于各个查询的耗时之和4100(2000+1000+500+600)毫秒,从运行结果可以看出,改成并行后整个查询耗时缩短到2124毫秒。
另一个常见的场景是多个线程需要等待统一的指令后同时执行,比如模拟高并发测试,多个线程同时对一个接口发起调用。
public class ConcurrentStartDemo {
public static void main(String[] args) throws InterruptedException {
int threads = 5;
// 开始信号
CountDownLatch startSignal = new CountDownLatch(1);
// 准备完成信号
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 (InterruptedException 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, 已准备就绪,等待开始信号...
主线程发出开始信号...
时间戳1746787080788,Thread-0 开始调用接口...
时间戳1746787080789,Thread-4 开始调用接口...
时间戳1746787080788,Thread-1 开始调用接口...
时间戳1746787080788,Thread-2 开始调用接口...
时间戳1746787080788,Thread-3 开始调用接口...
所有子线程已完成接口调用...
注意事项
CountDownLatch的await()方法和Object中的wait()方法极为相似,一不小心很容易调用到wait()方法。两个方法都是可以让线程进入等待状态,但是两者的底层实现线程等待的方式还是有区别的,根据已掌握的知识,我们总结下两者的部分区别,后面深入学习后再进一步总结。
(1)两者都可以迫使当前线程进入等待状态,两者都可以通过重载的方法指定等待时间。
(2)两者都可以抛出中断异常。
(3)wait()是java.lang.Object类的方法,java中所有对象都继承该方法;await()是java.util.concurrent.CountDownLatch类中的方法。
(4)调用wait()方法前,线程必须先通过synchronized获得锁;而await()方法不用。
(5)调用wait()方法的线程需要其他线程调用notify()/notifyAll()唤醒,而调用await()方法的线程在计数器减到0后自动唤醒。
总结
本文分析了CountDownLatch的的主要方法和使用场景,其主要应用场景是主线程等待各个子线程执行完成后继续执行、多个线程需要等待统一的指令后同时执行。最后总结了CountDownLatch的await()方法和Object中的wait()方法的一些区别。
如果文章对您有帮助,不妨“帧栈”一下,关注“帧栈”公众号,第一时间获取推送文章,您的肯定将是我写作的动力!