基础场景如下:用户在银行办理银行卡,银行卡办理完成后,可基于该卡办理会员服务,也可以基于该卡申请信用卡,最终通知用户全部办理完成。根据场景分析得知以下的流程图,办理银行卡必须串行执行,而后两个可以并行执行。
先来模拟一下业务代码,三个方法分别代表办理银行卡、办理会员、申请信用卡,其中办理银行卡为了模拟较长的响应时间设置延迟3秒,其他方法延迟1秒。
public BankCardInfo applyBankCard(BankCardInfo info) {
// TODO 设置银行名+密码,随机生成银行卡号并返回完整信息
String bankCardNum = UUID.randomUUID().toString().replace("-", "");
info.setBankCardNum(bankCardNum);
try {
// 模拟延迟3秒
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
log.info("银行卡申请成功!");
return info;
}
public String registerUser(BankCardInfo cardInfo) {
try {
// 模拟延迟1秒
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
if (Objects.isNull(cardInfo.getBankCardNum())) {
log.warning("银行卡尚未申请成功,用户注册失败");
return "error";
}
UserInfo userInfo = new UserInfo();
userInfo.setAge(18);
userInfo.setBankCardNum(cardInfo.getBankCardNum());
userInfo.setPassword("a1b2c3d4");
userInfo.setUserName("user1");
userInfo.setSexual("man");
log.info("用户注册成功!");
return "ok";
}
public String applyCreditCard(BankCardInfo cardInfo) {
try {
// 模拟延迟1秒
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
if (Objects.isNull(cardInfo.getBankCardNum())) {
log.warning("银行卡尚未申请成功,信用卡申请失败");
return "error";
}
CreditCardInfo creditCardInfo = new CreditCardInfo();
creditCardInfo.setBankCardNum(cardInfo.getBankCardNum());
creditCardInfo.setMoneyLimit(10000.00);
log.info("信用卡申请成功!");
return "ok";
}
场景1 异步任务(无回调)
这种情况针对主线程只完成最主要的任务,将其他任务提交给子线程后,就直接返回完成任务。这种方法最好是不要用在很核心的功能上,不然某个子线程异常回滚,其他线程又执行成功,你基本就可以去死了。
想象一下兑换物品的场景,子线程1扣积分,子线程2换东西,刚好积分数据库炸了,物品数据库猛扣,一夜之间物品全部被0元购。但是一般重要的业务会走异步回调,也不用太操心。
回到例子上,就让他办理银行卡成功后直接返回。 接下来编写执行全流程主方法,由于办理会员/申请信用卡依赖办理银行卡的返回值,所以给办理银行卡线程加上join(),在他执行完前阻塞其他线程。
public void applyMain() {
BankCardController controller = new BankCardController();
BankCardInfo cardInfo = new BankCardInfo("ChinaBank", null, 88888888, 0.00);
Thread cardThread = new Thread(() -> applyBankCard(cardInfo));
try {
cardThread.start();
cardThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread userThread = new Thread(() -> registerUser(cardInfo));
userThread.start();
Thread creditThread = new Thread(() -> applyCreditCard(cardInfo));
creditThread.start();
log.info("执行完毕,通知用户");
}
试着执行一下,日志信息如下。执行3秒后办理银行卡线程完成,主线程同时完成,1秒后另外两个子线程也执行完毕。这种场景其实比较简单随意,也不是很优雅,是直接阻塞线程;下一种场景我们采用更优雅的方式。
一月 31, 2023 4:22:20 下午 BankRegister.Controller.BankCardController applyBankCard
信息: 银行卡申请成功!
一月 31, 2023 4:22:20 下午 BankRegister.Controller.BankCardController applyMain
信息: 执行完毕,通知用户
一月 31, 2023 4:22:21 下午 BankRegister.Controller.BankCardController registerUser
信息: 用户注册成功!
一月 31, 2023 4:22:21 下午 BankRegister.Controller.BankCardController applyCreditCard
信息: 信用卡申请成功!
补充:Thread的join()方法底层是调用了Native wait()方法,是让当前线程等待,“当前线程”的意思就是你所处的线程,在例子中为Main线程。因此对子线程调用join()为让主线程等待子线程执行完毕,也就达到了cardThread执行完毕再继续执行的效果。
可以去SpringBoot中证实,在Service方法中执行Thread.currentThread.join(),当前线程毫无疑问是Main线程,被调用的线程也是Main线程,就陷入了死锁导致阻塞。
场景2 同步任务
这种场景也是日常最常见的,所有任务成功执行完后通知用户。还是刚刚的业务方法,但这次要对注册用户方法进行一定的改进。
在之前的方法中存在极大的缺陷,我们获取不到返回值,当返回error时仍然当做任务执行结束,从而继续向下执行。可以试验一下,在办理银行卡完成后手动将银行卡号置为null,尽管子线程都返回error,仍然会正常通知用户。很明显我们是需要返回值,且要依赖他来进行通知结果返回处理。
都说到这份上,很明显要引入Callable和Future了。既然要做,我们就优雅到极致:
- 新增用户注册/申请信用卡的返回值判断,动态返回通知用户语句;
- 新增办理银行卡返回时间限制,超时任务取消;
- 新增用户注册中的“用户名重复”异常结果;
- 引入线程池。
注册用户方法新增用户名重复判断,现在的逻辑为:输入用户名“admin”时,会直接返回error。
public String registerUser(BankCardInfo cardInfo, String userName) {
try {
// 模拟延迟1秒
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
if (Objects.isNull(cardInfo.getBankCardNum())) {
log.warning("银行卡尚未申请成功,用户注册失败");
return "error";
}
if ("admin".equals(userName)) {
log.warning("该用户名已存在,用户注册失败");
return "error";
}
UserInfo userInfo = new UserInfo();
userInfo.setAge(18);
userInfo.setBankCardNum(cardInfo.getBankCardNum());
userInfo.setPassword("a1b2c3d4");
userInfo.setUserName(userName);
userInfo.setSexual("man");
log.info("用户注册成功!");
return "ok";
}
主方法中引入了线程池,将3个工作线程修改为Callable形式并提交给线程池;为办理银行卡设置了5000ms的超时时间,增加了超时手动取消任务逻辑;在最后新增了对两个子线程返回值的判断,动态展示通知信息。
//线程池
private static final ExecutorService executor = Executors.newFixedThreadPool(5);
public void applyMain2() {
BankCardController controller = new BankCardController();
BankCardInfo cardInfo = new BankCardInfo("ChinaBank", null, 88888888, 0.00);
Callable<BankCardInfo> cardCallable = () -> applyBankCard(cardInfo);
Callable<String> registerCallable = () -> registerUser(cardInfo, "admin");
Callable<String> applyCreditCallable = () -> applyCreditCard(cardInfo);
Future<BankCardInfo> submit1 = executor.submit(cardCallable);
try {
BankCardInfo cardInfo1 = submit1.get(5000, TimeUnit.MILLISECONDS);
log.info(cardInfo1.toString());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
log.info("任务取消");
submit1.cancel(true);
}
String res1 = null;
String res2 = null;
Future<String> submit2 = executor.submit(registerCallable);
Future<String> submit3 = executor.submit(applyCreditCallable);
try {
res1 = submit2.get();
res2 = submit3.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
if ("ok".equals(res1) && "ok".equals(res2)) {
log.info("执行完毕,通知用户成功");
} else {
log.info("执行异常,通知用户失败");
}
}
由于我们将用户名传入了admin,返回的通知也成为了失败通知。在调小get()的超时时间后,线程超时捕获到TimeoutException,也成功手动取消了任务。
一月 31, 2023 8:02:10 下午 BankRegister.Controller.BankCardController applyBankCard
信息: 银行卡申请成功!
一月 31, 2023 8:02:10 下午 BankRegister.Controller.BankCardController registerUser
警告: 该用户名已存在,用户注册失败
一月 31, 2023 8:02:11 下午 BankRegister.Controller.BankCardController applyCreditCard
信息: 信用卡申请成功!
一月 31, 2023 8:02:11 下午 BankRegister.Controller.BankCardController applyMain2
信息: 执行异常,通知用户失败
但是仔细看一下执行时间就会发现还是有问题,办理会员和申请信用卡相差一秒,明显是串行了,因为get()方法也把他们俩阻塞住了。有一个不太合适的优化方法,就是循环调用Future的isDone()判断两个线程是否都执行结束,结束后再获取返回值。后面想办法会再优化这块(CompletableFuture或CountDownLatch),先实现一下。
try {
while(true) {
if (submit2.isDone() && submit3.isDone()) {
res1 = submit2.get();
res2 = submit3.get();
break;
} else {
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
如上面代码的逻辑,两线程状态都为完成后再获取返回值并跳出循环即可。
场景3 同步任务(优化)
场景2最后我们使用了一个不太优雅的方法,无限循环判断Future的isDone()状态,这对CPU的消耗是非常大的,需要优化,因此引入CountDownLatch。
他是一个多线程任务计数器,初始化一个数值并在子线程执行结束时手动-1,主线程中调用await()方法就可以在计数器清零前一直阻塞主线程执行。用法也很简单,只需要给每个子线程都传入该计数器,并在finally中显式调用countDown()使其减一即可。
try {
} catch (Exception e) {
e.printStackTrace();
} finally {
//手动减一
countDownLatch.countDown();
}
那么那段while(true)的丑陋循环就可以优化成,依据CountDownLatch的值是否归零,判断两个子线程执行完毕与否,完成再获取值来执行判断逻辑。
try {
//await()方法会在计数器清零前,一直阻塞主线程,直至子线程全部执行完毕
countDownLatch.await();
res1 = submit2.get();
res2 = submit3.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException ex) {
ex.printStackTrace();
}
基础的同步异步实现到这里就介绍完了,主要就是依靠FutureTask + Callable + CountDownLatch来实现。在SpringBoot中有更加方便的实现,但同时也会出现许多问题,因此放在下一篇中介绍。