前言
在日常开发过程中,多线程高并发程序的开发固然是必不可少的,但是想要对多线程技术应用得当并不是一件容易的事情。
随着Java版本的不断迭代,越来越多的并发工具逐渐被引入,尤其是从JDK1.5版本开始,这一方面极大的减轻了开发者的负担,另一方面又提高了高并发程序执行效率。
从今天的这篇文章开始,我会讲解JDK1.8的并发工具类的基本用法与使用场景、以及注意事项,针对这些并发工具的源码解析,我会放在整个并发核心库体系完毕之后讲解。
CountDownLatch
官方对它的介绍是:“CountDownLatch是一个同步助手,允许一个或多个线程等待一系列的其他线程执行结束”
CountDownLatch使用起来非常简单,就是一个工具类,可以帮助我们很优雅的解决主任务等待所有子任务都执行结束之后再进行下一步的场景。
具体使用步骤如下:
- 通过构造函数设定计数器阈值(不能小于0)
- countDown() 方法,该方法的主要作用是使构造CountDownLatch指定的count计数器减一;如果此时CountDownLatch中的计数器已经是0,这种情况下如果调用countDown()方法,则会被忽略,count不可能被减到小于0
- await()方法,会使当前调用的线程进入阻塞状态,直到count被减少到0为止,其他线程可以将当前线程中断;同样当count已经是0的时候,调用await()方法,则会被忽略,当前线程不会被阻塞。
- await(long timeout, TimeUnit unit)方法,是一个具备超时能力的阻塞方法,当时间达到给定的值后,不管计数器count的值是多少,当前线程都会退出阻塞。
- getCount()方法,可以返回当前计数器的值。
其常用API就以上这些,那我们来思考思考使用场景与实战。
由于CountDownLatch有可以让某些线程等待一些线程执行的特性,你有想到使用场景吗?
我来抛砖引玉,先讲讲我的使用场景吧。
在面试过程中不知道面试官有没有问到你这样一个问题:你们系统针对那种支付回调失败的订单是如何处理的呢?
你有被这样问到吗?
我曾经在某公司当面试官的时候有这样问应聘者;如果你没有被问到,那可能是没有遇到我吧!哈哈哈。
针对这个问题,不同的应聘者给出了不同的答案;也有应聘者不知道支付回调是什么,包括整个支付流程不清楚。
简单介绍微信支付流程
这里以微信支付主流程为例:
1、我们的系统调用微信统一下单接口(此过程我们会封装:appId、商户号、商户订单号、订单金额、货币类型、商品信息、openId、回调地址等相关参数,并对其生成签名)
2、以上接口会返回状态码、签名等支付报文信息,我们将支付报文给到前端
3、前端将支付报文通过支付sdk拉起微信支付
4、若用户完成了支付
5、微信系统会通过我们第一步配置的回调地址通知到我们系统
6、我们系统拿到支付结果,对订单进行后置处理
以上就是微信支付的基本流程,我们回到面试官的问题;
为什么面试官会抛出这样一个问题呢?
为什么会回调失败呢?
1、当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知。
2、我们系统的回调接口BUG,中途异常
3、等诸多莫名其妙的问题
当出现以上问题后,其实引发的是另外一个问题:订单支付超时问题。
一般情况下针对未支付的订单,15分钟后自动取消;那么如果是这种特殊原因导致的订单状态还是待支付状态,若直接关闭订单肯定会出现部分单边账情况(对于这样的用户来说,已经支付了订单,却系统显示待支付)
那可以怎么解决呢?
微信商户平台提供了查询订单接口,我们可以对订单进行最后确认,并通过最终的支付状态决定订单的后续操作。
以下是微信商户平台开放API列表以及官网地址,平常有参与支付或者想学习支付的小伙伴可以阅读官方文档:
pay.weixin.qq.com/wiki/doc/ap…
解决问题
以上介绍了微信支付流程,以及系统对出现问题后的容错机制,那和CountDownLatch有什么关系呢?
当我们对未支付订单进行校验,需要查询微信查询订单接口时,若是简简单单的循环处理效率太低,则可以选择使用CountDownLatch和多线程一起使用,提高执行效率;
那我们看下代码:
@Slf4j
public class CountDownLatchOrderPay {
/**
* 模拟注入接口
*/
private final static WeChatPayService weChatPayService = new WeChatPayService();
/**
* 模拟待支付订单号列表
*/
private final static List<String> ORDER_LIST = Arrays.asList("F13246", "F468986",
"F78546", "F57824", "F7996312", "F78544536", "F578458624", "F799637812", "F77898546", "F57824564", "F79963451312");
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(ORDER_LIST.size());
ORDER_LIST.forEach(
value -> {
new Thread(
() -> {
try {
Map<String, String> result = weChatPayService.checkPay(value);
} catch (Exception e) {
log.error("异常处理");
} finally {
latch.countDown();
}
}
).start();
}
);
try {
log.info("程序等待批量调用微信:{}", Thread.currentThread().getName());
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("获得结果集,继续执行后续任务:{}", Thread.currentThread().getName());
}
public static class WeChatPayService {
/**
* <p>
* 模拟调用微信订单查询接口
* 微信商户平台官方API: https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}
* </p>
*
* @param outTradeNo 商户订单号
* @return 模拟微信返回结果
*/
public Map<String,String> checkPay(String outTradeNo) {
try {
//模拟调用耗时
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("调用微信商户平台订单查询接口,订单号为:{}", outTradeNo);
HashMap<String, String> result = new HashMap<>();
result.put("out_trade_no", outTradeNo);
result.put("trade_state", "SUCCESS");
return result;
}
}
}
复制代码
以上代码简单模拟了并发调用微信接口,并主线程等待子线程统一执行结果的场景;这是一种解决思路,大家可以灵活运用。
注意:由于它的特性,await()方法是需要等待count减为0才释放,所以在调用countDown()时,我将其放在了finally语句块里面,防止由于异常原因导致count无法减至0,形成系统永远等待的场景,也可以合理使用超时等待机制。
上面我是通过循环创建的线程,其实不推荐这样使用,请大家使用线程池进行创建并管理线程;我为什么这样创建呢? 其实是我的一点点小心思(请原谅),后面我会详细讲解线程池的,这里就暂时不体现。
另外一种玩法
在介绍CountDownLatch的时候,就说到了它可以让一个或者多个线程进行等待
在模拟订单处理的场景下,我们用到是让主线程等待子线程执行完毕;那如何让多个线程等待一个线程执行完毕呢?
可能有很多人对CountDownLatch的印象是叫”发令枪”,对的,没错;的确可以形成一种发令枪的模式;
就好像田径运动,所有的参赛选手到起跑线进行等待,裁判打响发令枪时,所有选手同时起跑。
public static void main(String[] args) {
//count赋值为1
CountDownLatch latch = new CountDownLatch(1);
IntStream.range(0, 10).forEach(
value -> {
new Thread(
() -> {
try {
//等待号令
latch.await();
log.info("当前线程:{},开始起跑……", Thread.currentThread().getName());
} catch (InterruptedException e) {
log.error("异常处理……");
}
}
).start();
}
);
try {
//主线等待3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("Ready go……");
//开始发射
latch.countDown();
}
}
复制代码
执行结果如下:
[main] INFO com.xiaozhi.latch.CountDownLatchTest - Ready go……
[Thread-0] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-0,开始起跑……
[Thread-8] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-8,开始起跑……
[Thread-7] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-7,开始起跑……
[Thread-6] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-6,开始起跑……
[Thread-5] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-5,开始起跑……
[Thread-9] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-9,开始起跑……
[Thread-3] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-3,开始起跑……
[Thread-2] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-2,开始起跑……
[Thread-1] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-1,开始起跑……
[Thread-4] INFO com.xiaozhi.latch.CountDownLatchTest - 当前线程:Thread-4,开始起跑……
复制代码
观察以上代码,我们将CountDownLatch初始值设置为1,创建10个线程进行等待,主线程休眠3秒后调用countDown(),一声令下所有子线程开始执行。
各位读者,是不是感觉CountDownLatch的使用很简单呢?
但是有没有发现,count从初始值到0之后,整个latch的作用相当于结束了,不能重复利用;那我们下一节学习CyclicBarrier,它和CountDownLatch功能类似,但是可以重复利用。
今天对CountDownLatch的讲解就到此结束,我们下一篇文章见!
码云代码链接如下:
感谢阅读,祝大家工作愉快、身体健康!