聊聊并发编程的12种业务场景

本文探讨并发编程在实际项目中的12种应用,包括简单定时任务、监听器、日志收集、Excel导入、查询接口优化、用户上下文管理、模拟高并发、处理MQ消息、统计数量、延迟定时任务等。通过具体案例分析,展示多线程在不同场景下的使用策略和注意事项,旨在为开发者提供并发编程的实用参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

并发编程是一项非常重要的技术,无论在面试,还是工作中出现的频率非常高。

并发编程说白了就是多线程编程,但多线程一定比单线程效率更高?

答:不一定,要看具体业务场景。

毕竟如果使用了多线程,那么线程之间的竞争和抢占cpu资源,线程的上下文切换,也是相对来说比较耗时的操作。

下面这几个问题在面试中,你必定遇到过:

  1. 你在哪来业务场景中使用过多线程?

  2. 怎么用的?

  3. 踩过哪些坑?

今天聊聊我之前在项目中用并发编程的12种业务场景,给有需要的朋友一个参考。

1. 简单定时任务

各位亲爱的朋友,你没看错,Thread类真的能做定时任务。如果你看过一些定时任务框架的源码,你最后会发现,它们的底层也会使用Thread类。

实现这种定时任务的具体代码如下:

public static void init() {
    new Thread(() -> {
        while (true) {
            try {
                System.out.println("下载文件");
                Thread.sleep(1000 * 60 * 5);
            } catch (Exception e) {
                log.error(e);
            }
        }
    }).start();
}

使用Thread类可以做最简单的定时任务,在run方法中有个while的死循环(当然还有其他方式),执行我们自己的任务。有个需要特别注意的地方是,需要用try...catch捕获异常,否则如果出现异常,就直接退出循环,下次将无法继续执行了。

但这种方式做的定时任务,只能周期性执行,不能支持定时在某个时间点执行。

特别提醒一下,该线程建议定义成守护线程,可以通过setDaemon方法设置,让它在后台默默执行就好。

使用场景:比如项目中有时需要每隔5分钟去下载某个文件,或者每隔10分钟去读取模板文件生成静态html页面等等,一些简单的周期性任务场景。

使用Thread类做定时任务的优缺点:

  • 优点:这种定时任务非常简单,学习成本低,容易入手,对于那些简单的周期性任务,是个不错的选择。

  • 缺点:不支持指定某个时间点执行任务,不支持延迟执行等操作,功能过于单一,无法应对一些较为复杂的场景。

2.监听器

有时候,我们需要写个监听器,去监听某些数据的变化。

比如:我们在使用canal的时候,需要监听binlog的变化,能够及时把数据库中的数据,同步到另外一个业务数据库中。

​如果直接写一个监听器去监听数据就太没意思了,我们想实现这样一个功能:在配置中心有个开关,配置监听器是否开启,如果开启了使用单线程异步执行。

【文章福利】另外小编还整理了一些C++后台开发面试题,教学视频,后端学习路线图免费分享,需要的可以自行添加:Q群:720209036 点击加入~ 群文件共享

小编强力推荐C++后台开发免费学习地址:C/C++Linux服务器开发高级架构师/C++后台开发架构师​icon-default.png?t=M666https://ke.qq.com/course/417774?flowToken=1013189

​主要代码如下:

@Service
public CanalService {
    private volatile boolean running = false;
    private Thread thread;

    @Autowired
    private CanalConnector canalConnector;
    
    public void handle() {
        //连接canal
        while(running) {
           //业务处理
        }
    }
    
    public void start() {
       thread = new Thread(this::handle, "name");
       running = true;
       thread.start();
    }
    
    public void stop() {
       if(!running) {
          return;
       }
       running = false;
    }
}

在start方法中开启了一个线程,在该线程中异步执行handle方法的具体任务。然后通过调用stop方法,可以停止该线程。

其中,使用volatile关键字控制的running变量作为开关,它可以控制线程中的状态。

接下来,有个比较关键的点是:如何通过配置中心的配置,控制这个开关呢?

以apollo配置为例,我们在配置中心的后台,修改配置之后,自动获取最新配置的核心代码如下:

public class CanalConfig {
    @Autowired
    private CanalService canalService;

    @ApolloConfigChangeListener
    public void change(ConfigChangeEvent event) {
        String value = event.getChange("test.canal.enable").getNewValue();
        if(BooleanUtils.toBoolean(value)) {
            canalService.start();
        } else {
            canalService.stop();
        }
    }
}

通过apollo的ApolloConfigChangeListener注解,可以监听配置参数的变化。

如果test.canal.enable开关配置的true,则调用canalService类的start方法开启canal数据同步功能。如果开关配置的false,则调用canalService类的stop方法,自动停止canal数据同步功能。

3.收集日志

在某些高并发的场景中,我们需要收集部分用户的日志(比如:用户登录的日志),写到数据库中,以便于做分析。

但由于项目中,还没有引入消息中间件,比如:kafka、rocketmq等。

如果直接将日志同步写入数据库,可能会影响接口性能。

所以,大家很自然想到了异步处理。

实现这个需求最简单的做法是,开启一个线程,异步写入数据到数据库即可。

这样做,可以是可以。

但如果用户登录操作的耗时,比异步写入数据库的时间要少得多。这样导致的结果是:生产日志的速度,比消费日志的速度要快得多,最终的性能瓶颈在消费端。

<think>好的,用户现在想了解“结构化并发”,我需要详细解释这个概念。之前用户问过Vector API,现在转向并发编程,可能是在学习Java的高阶特性。首先,我得确认结构化并发的定义和核心思想。 结构化并发是一种管理并发的编程范式,强调任务的结构化生命周期,类似结构化编程对流程的控制。核心是子任务的生命周期不能超过父任务,防止线程泄漏和资源浪费。用户可能在使用多线程时遇到管理复杂的问题,所以需要结构化并发的优势来简化代码。 接下来,需要介绍它的设计目标,比如避免线程泄漏、提高可维护性、简化错误处理。然后对比传统并发的问题,比如线程失控、错误处理复杂、资源泄露。用户可能对传统并发的问题有体会,所以对比能帮助理解。 版本演进部分,用户之前的问题里有版本表格,可能需要类似的结构。Java的JEP 428在19孵化,JEP 453在20预览,21中再次预览,22引入最终API。这部分要准确,用户可能关注Java的实现情况。 核心特性包括作用域、取消传播、错误处理、资源管理。需要举例说明,比如使用StructuredTaskScope,代码示例能帮助用户理解。要提到子任务必须在父作用域内,自动传播取消,集中处理异常。 适用场景方面,用户可能想知道何时使用,比如微服务并行调用、批量处理、流水线作业。注意事项如避免阻塞操作、统一作用域、超时设置,这些是实际使用中的常见问题,需要提醒用户。 对比传统并发,要突出结构化并发的优势,比如生命周期管理、错误处理、可读性。用户可能之前用ExecutorService,现在想了解更结构化的方式。 最后,未来方向可能和Project Loom相关,虚拟线程的集成,用户可能关心Java的未来发展。 需要确保语言口语化,避免术语堆砌,结构清晰,分点讲解。检查是否有遗漏点,比如代码示例的正确性,版本信息的准确性。用户可能需要实际应用,所以强调实用场景和注意事项。</think>以下是关于 **结构化并发(Structured Concurrency)** 的详细解析(聚焦其设计哲学、核心机制及在 Java 中的实现): --- ### **1. 什么是结构化并发?** **目标**:通过 **任务生命周期嵌套** 的编程模型,确保并发任务(如线程、协程)的代码结构与控制流逻辑一致,避免传统并发编程中常见的“线程泄漏”和“资源失控”问题。 **核心思想**: - 子任务的生命周期必须严格嵌套在父任务的作用域内(类似 `try-with-resources` 对资源的管控)。 - 所有子任务的完成或失败必须被显式观察(“没有任务会被遗忘”)。 例如: ```java try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<Integer> task1 = scope.fork(() -> fetchDataFromA()); Future<String> task2 = scope.fork(() -> fetchDataFromB()); scope.join(); // 等待所有子任务完成 scope.throwIfFailed(); // 统一处理异常 return combine(task1.resultNow(), task2.resultNow()); } // 作用域结束时,自动取消未完成的任务 ``` --- ### **2. 设计动机** #### **传统并发的问题** - **线程泄漏**:子线程脱离父线程控制,无法保证清理(如父任务结束后子线程仍在运行)。 - **错误处理复杂**:子任务异常难以追踪和聚合。 - **资源竞争**:任务间依赖管理混乱,导致死锁或资源未释放。 #### **结构化并发的优势** - **代码可读性**:并发逻辑与代码结构一致,支持“代码即文档”。 - **可靠性**:通过作用域强制管理任务生命周期,避免资源泄漏。 - **错误传播**:子任务异常自动传播到父作用域,支持集中处理。 --- ### **3. Java 中的实现(Project Loom)** #### **版本演进** | JDK 版本 | 状态 | 关键 API | |----------|-----------|----------------------------------| | **19** | 孵化 | `StructuredTaskScope`(JEP 428) | | **20** | 预览 | 改进 API(JEP 453) | | **21** | 二次预览 | 简化异常处理 | | **22** | 正式发布 | 纳入 `java.util.concurrent` | #### **核心类** 1. **`StructuredTaskScope<T>`** - 定义并发任务的作用域,所有子任务必须在此作用域内创建。 - 支持两种策略: - `ShutdownOnFailure()`:任一子任务失败时取消其他任务。 - `ShutdownOnSuccess()`:任一子任务成功时取消其他任务。 2. **`Future<T>`** - 表示一个子任务,通过 `fork()` 提交到作用域。 --- ### **4. 核心特性** #### **(1) 作用域(Scope)绑定** - 所有子任务必须在显式的作用域内启动。 - 作用域结束时,自动取消未完成的任务(通过 `AutoCloseable` 实现)。 #### **(2) 取消传播** - 父任务取消时,所有子任务级联取消。 - 支持超时机制:`scope.joinUntil(Instant deadline)`。 #### **(3) 错误聚合** - 子任务异常通过 `throwIfFailed()` 统一抛出,保留所有异常的堆栈信息。 #### **(4) 资源安全** - 任务与作用域绑定,确保线程池、文件句柄等资源随作用域关闭自动释放。 --- ### **5. 适用场景** 1. **并行调用微服务**:同时请求多个 API 并聚合结果。 2. **批量数据处理**:拆分大任务为子任务并行处理。 3. **流水线作业**:多个阶段任务按依赖关系组织。 --- ### **6. 代码示例** ```java // 并行执行两个 HTTP 请求,任一失败则整体失败 String fetchCombinedData() throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<String> userTask = scope.fork(() -> httpClient.get("/user")); Future<String> orderTask = scope.fork(() -> httpClient.get("/order")); scope.join(); // 等待所有任务完成 scope.throwIfFailed(); // 检查异常 return userTask.resultNow() + orderTask.resultNow(); } // 作用域关闭时自动取消残留任务 } ``` --- ### **7. 与传统并发的对比** | **维度** | **结构化并发** | **传统并发(如 ExecutorService)** | |-------------------|----------------------------------------|-----------------------------------------| | **生命周期管理** | 通过作用域自动绑定父-子任务关系 | 需手动管理线程池关闭和任务取消 | | **错误处理** | 异常自动聚合到父作用域 | 需单独捕获每个 Future 的异常 | | **可读性** | 代码结构反映任务逻辑层级 | 回调地狱或分散的 Future 处理逻辑 | | **资源泄漏风险** | 低(作用域强制清理) | 高(易遗漏线程池关闭或任务取消) | --- ### **8. 注意事项** 1. **避免阻塞操作**:在子任务中阻塞可能影响作用域关闭的及时性。 2. **统一作用域**:同一层级的任务应共享一个作用域。 3. **超时设置**:建议为 `join()` 或 `joinUntil()` 设置超时,防止死锁。 --- ### **9. 未来方向** - **与虚拟线程(Virtual Threads)深度集成**:通过 Project Loom 实现轻量级并发(数千个虚拟线程)。 - **更细粒度的监控**:集成 JDK Flight Recorder 跟踪任务生命周期。 --- 结构化并发通过“作用域”这一抽象,将并发任务的组织方式与代码结构对齐,显著提升了复杂并发程序的可维护性和可靠性。如需深入实践,可参考 [JEP 453](https://openjdk.org/jeps/453)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值