CountDownLatch的概念:允许一个或者多个线程去等待其他线程完成操作。构造函数允许接收一个int值,作为初始线程数量,也可以认为是倒数几个数。
线程执行完毕后,通过 countDown() 方法,对构造中的count值进行-1操作,等到count值是0后,CountDownLatch才会释放所有线程的等待。
还有 await() 方法,用这个方法让全部的线程进入等待。
这两个方法就是 CountDownLatch 的关键方法。
由于项目中存在一个自动更新客户等级需求,定时(每天凌晨)对客户数据进行晋升与降级操作,该任务分为四个阶段:
获取四个月的所有客户相关的订单数据,并保存并更新自动晋升表
客户晋升(成交晋升长期、FANS)
客户降级为成交
删除四个月前的订单数据(晋升表)
为了优化代码执行速度,所以在第一阶段代码中使用了CountDownLatch来对定时任务的更新数据部分进行多线程处理,并等待线程全部结束后释放线程并继续执行主线程的操作(晋升与降级)。
使用了CountDownLatch和Thread的组合,确实让接口的耗时降低了许多。但是也为我这次遇到的bug埋下了隐患。
由于定时任务是在每天凌晨执行的,第二天上班去看了一下定时任务执行状态,发现调度成功了,但是任务没有结束;这个时候就开始排查日志来试图找出问题所在,发现线程在修改数据完成之后主线程进入了线程等待阶段,一直没有释放。看一下问题代码:
//大于2000则使用线程更新数据
if (orderDataOfTheCurrentMonthData.size() > 2000) {
//设置线程数
CountDownLatch latch = new CountDownLatch(10); // 问题部分
List> partition = Lists.partition(orderDataOfTheCurrentMonthData, 2000);
for (List autoPromotionGradePOList : partition) {
taskExecutor.execute(() -> {
saveOrUpdatePromotionData(autoPromotionGradePOList);
//单次任务结束,计数器减一
latch.countDown();
});
}
try {
// 等待所有任务结束
latch.await();
} catch (InterruptedException e) {
log.error("automatically update customer levels error msg :", e);
throw new BizException(50000, "线程任务释放异常");
} finally {
log.info("The task of updating automatic promotion data is completed elapsed time :{}", stopwatch.elapsed(TimeUnit.MICROSECONDS));
}
} else {
saveOrUpdatePromotionData(orderDataOfTheCurrentMonthData);
}
经过Debug之后发现线程一直没释放,原因是创建CountDownLatch时初始化初始化值给了10;本次任务执行的时候当月订单数据没有达到20000,所以线程使用数上没有超过预设的的值10,导致了线程只释放了7个,还剩下3个没有被释放,就导致**latch.await()**这一步一直在等待这所有线程数释放为0。
由于定时任务时单机串行的所以会导致后面任务阻塞!
下面时官方描述:
A CountDownLatch is initialized with a given count. The await methods block until the current count reaches zero due to invocations of the countDown method, after which all waiting threads are released and any subsequent invocations of 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 CyclicBarrier.
A CountDownLatch is a versatile synchronization tool and can be used for a number of purposes. A CountDownLatch initialized with a count of one serves as a simple on/off latch, or gate: all threads invoking await wait at the gate until it is opened by a thread invoking countDown. A CountDownLatch initialized to N can be used to make one thread wait until N threads have completed some action, or some action has been completed N times.
A useful property of a CountDownLatch is that it doesn’t require that threads calling countDown wait for the count to reach zero before proceeding, it simply prevents any thread from proceeding past an await until all threads could pass.
CountDownLatch用作简单的开/关锁存器或门:所有调用await的线程都在门处等待,直到由调用countDown的线程打开它。
相当于CountDownLatch是一个导游,他需要等待他的十个游客全部到齐了才会开始接下来的行程。
既然发现了问题,那么解决起来就很简单了。
最简单的方法就是根据数量来计算需要使用线程数然后设置初始化值,也可以通过设定好的初始化值来分割数据。
通过修改latch.await(60,TimeUnit.SECONDS)来设置超时等待多少秒来释放线程。
两者都能解决问题,推荐使用第一种,毕竟规范的使用工具才是一个程序员该做的。第二种视情况使用,也可以都是用,由于我这个任务不需要设置超时释放,所以选择了第一种。
private static final int COUNT = 2000;
...
//大于2000则使用线程更新数据
if (orderDataOfTheCurrentMonthData.size() > COUNT) {
//设置所需线程
int i = (orderDataOfTheCurrentMonthData.size() / COUNT) + 1;
CountDownLatch latch = new CountDownLatch(i);
List> partition = Lists.partition(orderDataOfTheCurrentMonthData, COUNT);
for (List autoPromotionGradePOList : partition) {
taskExecutor.execute(() -> {
saveOrUpdatePromotionData(autoPromotionGradePOList);
//单次任务结束,计数器减一
latch.countDown();
});
}
try {
// 等待所有任务结束
latch.await();
} catch (InterruptedException e) {
log.error("automatically update customer levels error msg :", e);
throw new BizException(50000, "线程任务释放异常");
} finally {
log.info("The task of updating automatic promotion data is completed elapsed time :{}", stopwatch.elapsed(TimeUnit.MICROSECONDS));
}
} else {
saveOrUpdatePromotionData(orderDataOfTheCurrentMonthData);
}
出现这种问题实属不该,本次记录下来用于铭记自己出现了这种不规范的使用。
本次BUG没有什么好用的总结,那就浅浅记录一下,希望以后不会再犯了!!!