前言
很多时候,我们为了提升接口的性能,会把之前单线程同步
执行的代码,改成多线程异步
执行。
比如:查询用户信息接口,需要返回用户基本信息、积分信息、成长值信息,而用户、积分和成长值,需要调用不同的接口获取数据。
如果查询用户信息接口,同步调用
三个接口获取数据,会非常耗时。
这就非常有必要把三个接口调用,改成异步调用
,最后汇总结果
。
再比如:注册用户接口,该接口主要包含:写用户表,分配权限,配置用户导航页,发通知消息等功能。
该用户注册接口包含的业务逻辑比较多,如果在接口中同步执行这些代码,该接口响应时间会非常慢。
这时就需要把业务逻辑梳理一下,划分:核心逻辑
和非核心逻辑
。这个例子中的核心逻辑是:写用户表和分配权限,非核心逻辑是:配置用户导航页和发通知消息。
显然核心逻辑
必须在接口中同步执行
,而非核心逻辑
可以多线程异步
执行。
等等。
需要使用多线程的业务场景太多了,使用多线程异步执行的好处不言而喻。
但我要说的是,如果多线程没有使用好,它也会给我们带来很多意想不到的问题,不信往后继续看。
今天跟大家一起聊聊,代码改成多线程调用之后,带来的9大问题。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
1.获取不到返回值
如果你通过直接继承Thread
类,或者实现Runnable
接口的方式去创建线程
。
那么,恭喜你,你将没法获取该线程方法的返回值。
使用线程的场景有两种:
- 不需要关注线程方法的返回值。
- 需要关注线程方法的返回值。
大部分业务场景是不需要关注线程方法返回值的,但如果我们有些业务需要关注线程方法的返回值该怎么处理呢?
查询用户信息接口,需要返回用户基本信息、积分信息、成长值信息,而用户、积分和成长值,需要调用不同的接口获取数据。
如下图所示:
在Java8之前可以通过实现Callable
接口,获取线程返回结果。
Java8以后通过CompleteFuture
类实现该功能。我们这里以CompleteFuture为例:
public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException { final UserInfo userInfo = new UserInfo(); CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> { getRemoteUserAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> { getRemoteBonusAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> { getRemoteGrowthAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join(); userFuture.get(); bonusFuture.get(); growthFuture.get(); return userInfo; }
温馨提醒一下,这两种方式别忘了使用线程池。示例中我用到了executor,表示自定义的线程池,为了防止高并发场景下,出现线程过多的问题。
此外,Fork/join
框架也提供了执行任务并返回结果的能力。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
2.数据丢失
我们还是以注册用户接口为例,该接口主要包含:写用户表,分配权限,配置用户导航页,发通知消息等功能。
其中:写用户表和分配权限功能,需要在一个事务中同步执行。而剩余的配置用户导航页和发通知消息功能,使用多线程异步执行。
表面上看起来没问题。
但如果前面的写用户表和分配权限功能成功了,用户注册接口就直接返回成功了。
但如果后面异步执行的配置用户导航页,或发通知消息功能失败了,怎么办?
如下图所示:
该接口前面明明已经提示用户成功了,但结果后面又有一部分功能在多线程异步执行中失败了。
这时该如何处理呢?
没错,你可以做失败重试
。
但如果重试了一定的次数,还是没有成功,这条请求数据该如何处理呢?如果不做任何处理,该数据是不是就丢掉了?
为了防止数据丢失,可以用如下方案:
- 使用mq异步处理。在分配权限之后,发送一条mq消息,到mq服务器,然后在mq的消费者中使用多线程,去配置用户导航页和发通知消息。如果mq消费者中处理失败了,可以自己重试。
- 使用job异步处理。在分配权限之后,往任务表中写一条数据。然后有个job定时扫描该表,然后配置用户导航页和发通知消息。如果job处理某条数据失败了,可以在表中记录一个重试次数,然后不断重试。但该方案有个缺点,就是实时性可能不太高。
3.顺序问题
如果你使用了多线程,就必须接受一个非常现实的问题,即顺序问题
。
假如之前代码的执行顺序是:a,b,c,改成多线程执行之后,代码的执行顺序可能变成了:a,c,b。(这个跟cpu调度算法有关)
例如:
public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.println("a")); Thread thread2 = new Thread(() -> System.out.println("b")); Thread thread3 = new Thread(() -> System.out.println("c")); thread1.start(); thread2.start(); thread3.start(); }
执行结果:
a c b
那么,来自灵魂的一问:如何保证线程的顺序呢?
即线程启动的顺序是:a,b,c,执行的顺序也是:a,b,c。
如下图所示:
3.1 join
Thread
类的join
方法它会让主线程等待子线程运行结束后,才能继续运行。
列如:
public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> System.out.println("a")); Thread thread2 = new Thread(() -> System.out.println("b")); Thread thread3 = new Thread(() -> System.out.println("c")); thread1.start(); thread1.join(); thread2.start(); thread2.join(); thread3.start(); }
执行结果永远都是:
a b c
3.2 newSingleThreadExecutor
我们可以使用JDK自带的Excutors
类的newSingleThreadExecutor
方法,创建一个单线程
的线程池
。
例如:
public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); Thread thread1 = new Thread(() -> System.out.println("a")); Thread thread2 = new Thread(() -> System.out.println("b")); Thread thread3 = new Thread(() -> System.out.println("c")); executorService.submit(thread1); executorService.submit(thread2); executorService.submit(thread3); executorService.shutdown(); }
执行结果永远都是:
a b c
使用Excutors
类的newSingleThreadExecutor
方法创建的单线程的线程池,使用了LinkedBlockingQueue
作为队列,而此队列按 FIFO
(先进先出)排序元素。
添加到队列的顺序是a,b,c,则执行的顺序也是a,b,c。
3.3 CountDownLatch
CountDownLatch
是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行。
例如:
public class ThreadTest { public static void main(String[] args) throws InterruptedException { CountDownLatch latch1 = new CountDownLatch(0); CountDownLatch latch2 = new CountDownLatch(1); CountDownLatch latch3 = new CountDownLatch(1); Thread thread1 = new Thread(new TestRunnable(latch1, latch2, "a")); Thread thread2 = new Thread(new TestRunnable(latch2, latch3, "b")); Thread thread3 = new Thread(new TestRunnable(latch3, latch3, "c")); thread1.start(); thread2.start(); thread3.start(); } } class TestRunnable implements Runnable { private CountDownLatch latch1; private CountDownLatch latch2; private String message; TestRunnable(CountDownLatch latch1, CountDownLatch latch2, String message) { this.latch1 = latch1; this.latch2 = latch2; this.message = message; } @Override public void run() { try { latch1.await(); System.out.println(message); } catch (InterruptedException e) { e.printStackTrace(); } latch2.countDown(); } }
执行结果永远都是:
a b c
此外,使用CompletableFuture
的thenRun
方法,也能多线程的执行顺序,在这里就不一一介绍了。