麻了,代码改成多线程,竟有9大问题 上

简介: 麻了,代码改成多线程,竟有9大问题 上


前言

很多时候,我们为了提升接口的性能,会把之前单线程同步执行的代码,改成多线程异步执行。

比如:查询用户信息接口,需要返回用户基本信息、积分信息、成长值信息,而用户、积分和成长值,需要调用不同的接口获取数据。

如果查询用户信息接口,同步调用三个接口获取数据,会非常耗时。

这就非常有必要把三个接口调用,改成异步调用,最后汇总结果

再比如:注册用户接口,该接口主要包含:写用户表,分配权限,配置用户导航页,发通知消息等功能。

该用户注册接口包含的业务逻辑比较多,如果在接口中同步执行这些代码,该接口响应时间会非常慢。

这时就需要把业务逻辑梳理一下,划分:核心逻辑非核心逻辑。这个例子中的核心逻辑是:写用户表和分配权限,非核心逻辑是:配置用户导航页和发通知消息。

显然核心逻辑必须在接口中同步执行,而非核心逻辑可以多线程异步执行。

等等。

需要使用多线程的业务场景太多了,使用多线程异步执行的好处不言而喻。

但我要说的是,如果多线程没有使用好,它也会给我们带来很多意想不到的问题,不信往后继续看。

今天跟大家一起聊聊,代码改成多线程调用之后,带来的9大问题。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

1.获取不到返回值

如果你通过直接继承Thread类,或者实现Runnable接口的方式去创建线程

那么,恭喜你,你将没法获取该线程方法的返回值。

使用线程的场景有两种:

  1. 不需要关注线程方法的返回值。
  2. 需要关注线程方法的返回值。

大部分业务场景是不需要关注线程方法返回值的,但如果我们有些业务需要关注线程方法的返回值该怎么处理呢?

查询用户信息接口,需要返回用户基本信息、积分信息、成长值信息,而用户、积分和成长值,需要调用不同的接口获取数据。

如下图所示:

在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.数据丢失

我们还是以注册用户接口为例,该接口主要包含:写用户表,分配权限,配置用户导航页,发通知消息等功能。

其中:写用户表和分配权限功能,需要在一个事务中同步执行。而剩余的配置用户导航页和发通知消息功能,使用多线程异步执行。

表面上看起来没问题。

但如果前面的写用户表和分配权限功能成功了,用户注册接口就直接返回成功了。

但如果后面异步执行的配置用户导航页,或发通知消息功能失败了,怎么办?

如下图所示:

该接口前面明明已经提示用户成功了,但结果后面又有一部分功能在多线程异步执行中失败了。

这时该如何处理呢?

没错,你可以做失败重试

但如果重试了一定的次数,还是没有成功,这条请求数据该如何处理呢?如果不做任何处理,该数据是不是就丢掉了?

为了防止数据丢失,可以用如下方案:

  1. 使用mq异步处理。在分配权限之后,发送一条mq消息,到mq服务器,然后在mq的消费者中使用多线程,去配置用户导航页和发通知消息。如果mq消费者中处理失败了,可以自己重试。
  2. 使用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

此外,使用CompletableFuturethenRun方法,也能多线程的执行顺序,在这里就不一一介绍了。

相关实践学习
消息队列RocketMQ版:基础消息收发功能体验
本实验场景介绍消息队列RocketMQ版的基础消息收发功能,涵盖实例创建、Topic、Group资源创建以及消息收发体验等基础功能模块。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
7月前
多线程案例-定时器(附完整代码)
多线程案例-定时器(附完整代码)
306 0
|
7月前
|
Java
|
25天前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
37 6
|
5月前
|
安全 Python
告别低效编程!Python线程与进程并发技术详解,让你的代码飞起来!
【7月更文挑战第9天】Python并发编程提升效率:**理解并发与并行,线程借助`threading`模块处理IO密集型任务,受限于GIL;进程用`multiprocessing`实现并行,绕过GIL限制。示例展示线程和进程创建及同步。选择合适模型,注意线程安全,利用多核,优化性能,实现高效并发编程。
78 3
|
7月前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
236 2
|
7月前
|
安全 Java
Java多线程基础-10:代码案例之定时器(一)
`Timer` 是 Java 中的一个定时器类,用于在指定延迟后执行指定的任务。它常用于实现定时任务,例如在网络通信中设置超时或定期清理数据。`Timer` 的核心方法是 `schedule()`,它可以安排任务在延迟一段时间后执行。`
149 1
|
4月前
|
Java Windows
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
|
4月前
|
开发者 C# 存储
WPF开发者必读:资源字典应用秘籍,轻松实现样式与模板共享,让你的WPF应用更上一层楼!
【8月更文挑战第31天】在WPF开发中,资源字典是一种强大的工具,用于共享样式、模板、图像等资源,提高了应用的可维护性和可扩展性。本文介绍了资源字典的基础知识、创建方法及最佳实践,并通过示例展示了如何在项目中有效利用资源字典,实现资源的重用和动态绑定。
114 0
|
4月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
49 0
|
6月前
|
API
linux---线程互斥锁总结及代码实现
linux---线程互斥锁总结及代码实现