"/>
侧边栏壁纸
博主头像
PySuper 博主等级

千里之行,始于足下

  • 累计撰写 286 篇文章
  • 累计创建 17 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

Java 线程池

PySuper
2022-11-24 / 0 评论 / 0 点赞 / 7 阅读 / 0 字
温馨提示:
所有牛逼的人都有一段苦逼的岁月。 但是你只要像SB一样去坚持,终将牛逼!!! ✊✊✊

线程池

拒接策略

线程池工作队列满了有哪些拒接策略?

  1. AbortPolicy (中止策略)

    • 行为:直接抛出 RejectedExecutionException 异常中断任务提交

    • 适用场景:对任务完整性要求极高的场景(如金融交易),需配合异常捕获机制

    • 风险:未捕获异常可能导致程序中断

  2. CallerRunsPolicy (调用者运行策略)

    • 行为:将任务回退到提交任务的线程(如主线程)直接执行

    • 适用场景:生产速度远高于消费速度时自然限流,允许短暂阻塞主线程的非实时任务

    • 注意事项:避免关键线程(如 UI 主线程)被阻塞

  3. DiscardPolicy (静默丢弃策略)

    • 行为:无提示丢弃新任务,不抛异常也不执行

    • 适用场景:允许任务丢失的非关键场景(如实时监控采样)

    • 风险:需监控丢弃任务量,避免数据不一致

  4. DiscardOldestPolicy (弃老策略)

    • 行为:丢弃队列中最旧任务,重试提交当前新任务

    • 适用场景:新任务优先级高于旧任务(如实时消息覆盖历史数据)

    • 风险:可能丢弃重要旧任务,需评估业务容忍度

  5. 自定义拒绝策略

    • 实现方式:实现 RejectedExecutionHandler 接口扩展逻辑

    • 典型方案:

      • 持久化存储:任务存入数据库/Redis 后续处理

      • 异步重试:延迟后重新提交或转移至备用线程池

      • 告警通知:触发监控并记录任务详情

    • 适用场景:需保障任务最终执行的业务(如订单支付)

  6. 策略选择原则

    • 任务重要性:关键任务优先用 CallerRunsPolicy 或自定义策略

    • 系统容忍度:允许丢失用 DiscardPolicy,需时效性用 DiscardOldestPolicy

    • 资源限制:内存敏感场景避免无界队列,明确队列容量

    • 监控配套:静默丢弃策略需加强队列长度和拒绝次数监控

核心线程数

核心线程数设置为 0 可不可以?

  1. 可以设置但需谨慎权衡

    • 技术上允许将核心线程数设为 0,线程池通过非核心线程处理任务

    • 任务提交逻辑

      • 当前工作线程为 0 时创建非核心线程执行任务

      • 已有非核心线程且队列未满时,任务入队等待

    • 适用场景

      • 极低频非关键任务(如日志清理),避免常驻线程占用资源

      • 突发流量后需快速释放资源的临时任务(如活动促销统计)

  2. 潜在风险与限制

    • 线程无法复用:每次任务可能触发新建线程,增加创建/销毁开销

    • 队列容量敏感

      • 无界队列(如 LinkedBlockingQueue)可能导致 OOM

      • 有界队列需精确设置容量防止任务堆积

    • 响应延迟:首次任务需等待线程创建,影响实时性要求高的场景

  3. 替代优化方案

    • 动态核心线程:通过 allowCoreThreadTimeOut(true) 允许核心线程超时回收,保留复用能力

    • 弹性线程池

      • 根据 QPS 动态调整 corePoolSize(如夜间降为 0,高峰期恢复)

      • 结合监控告警自动扩容/缩容(如 Prometheus + 动态配置中心)

  4. 实践建议

    • 稳定性要求高的系统(如支付服务)避免核心线程数为 0,优先动态调整策略

    • 测试环境需验证线程创建速率和任务响应时间,防止生产环境性能瓶颈

shutdown & shutdownNow

线程池中 shutdown (),shutdownNow() 这两个方法有什么作用?

  1. shutdown() 的作用

    • 新任务处理:立即拒绝新任务提交,触发拒绝策略(如抛 RejectedExecutionException

    • 现有任务处理:等待已提交任务(包括执行中和队列中的)全部完成

    • 状态变化:将线程池状态从 RUNNING 变为 SHUTDOWN,最终进入 TERMINATED

    • 线程中断:仅中断空闲线程,不干扰执行中的任务

  2. shutdownNow() 的作用

    • 新任务处理:立即拒绝新任务提交。

    • 现有任务处理

      • 尝试中断所有正在执行的任务(通过 interrupt()

      • 清空队列并返回未执行任务列表(List<Runnable>

    • 状态变化:将状态从 RUNNING 改为 STOP,最终进入 TERMINATED

    • 线程中断:强制中断所有工作线程,无论是否空闲

  3. 关键差异对比

    • 新任务处理:均拒绝新任务

    • 现有任务处理

      • shutdown() 等待任务完成

      • shutdownNow() 中断任务并丢弃未执行任务

    • 线程中断范围

      • shutdown() 仅中断空闲线程

      • shutdownNow() 中断所有线程

    • 返回值

      • shutdown() 无返回值

      • shutdownNow() 返回未执行任务列表

    • 适用场景

      • shutdown():优雅停机(如服务正常退出)

      • shutdownNow():紧急终止(如死锁或资源耗尽)

  4. 使用建议

    • 组合调用:先调用 shutdown() 等待任务完成,若超时未终止再调用 shutdownNow(),需捕获 InterruptedException 并处理。

    • 任务容错设计:在任务代码中检查中断状态,确保能响应中断请求

  5. 注意事项

    • 资源泄漏:未正确关闭线程池可能导致线程和资源(如数据库连接)无法释放

    • 队列影响

      • 无界队列使用 shutdown() 可能导致 OOM

      • shutdownNow() 清空队列可缓解内存压力

线程任务撤回

提交给线程池中的任务可以被撤回吗?

  1. 通过 Future 对象取消任务

    • 提交任务后返回 Future 对象,调用 cancel(boolean mayInterruptIfRunning) 可尝试取消任务

    • 参数作用

      • mayInterruptIfRunning = true:若任务已执行,触发线程中断(需任务代码响应中断)

      • mayInterruptIfRunning = false:仅取消未开始执行的任务

    • 返回值

      • true:任务成功取消(未开始或已响应中断)

      • false:任务已完成或无法取消

  2. 不同任务状态下的撤回结果

    • 任务未开始执行cancel() 可成功移除队列中的任务

    • 任务正在执行:依赖任务代码的中断响应逻辑(如检查 isInterrupted() 或捕获 InterruptedException

    • 任务已完成:无法撤回,cancel() 返回 false

  3. 线程池实现的影响

    • ThreadPoolExecutor 的局限性

      • 无界队列(如 LinkedBlockingQueue)中的任务无法直接撤回,需通过 Future.cancel()

      • remove(Runnable task) 方法可移除队列中未执行的任务,但需明确任务引用

    • 定时任务(ScheduledThreadPool

      • ScheduledFuture.cancel() 可终止周期性任务,已开始的单次任务仍需中断响应逻辑

  4. 实践建议

    • 代码容错设计:任务逻辑中定期检查中断状态,确保能响应取消请求

    • 组合关闭策略

      • 先调用 shutdown() 等待任务完成,超时后使用 shutdownNow() 强制终止

    • 避免无界队列:使用有界队列(如 ArrayBlockingQueue)减少无法撤回任务的风险

使用场景

奇偶数

多线程打印奇偶数,怎么控制打印的顺序

  1. 基础同步锁方案

    • 同步机制:使用 synchronized 保证原子操作,避免竞态条件

    • 双重检查机制

      • 外层 while(num <= 100) 控制整体循环

      • 内层 while(num%2 ==0) 实现条件等待

    • 线程协作

      • wait() 释放锁并暂停当前线程

      • notify() 唤醒等待线程

  2. 标志位优化方案

    • 显式状态控制:通过 isOddTurn 布尔标志管理奇偶切换

    • 性能优化

      • 避免频繁计算奇偶性,提升执行效率

      • 便于扩展多线程交替逻辑(如三线程 ABC 交替)

  3. 高级锁机制方案

    • 精细控制:使用 ReentrantLock 替代 synchronized,提供更灵活的锁管理

    • 条件队列

      • 通过 Condition 对象(oddCondevenCond)分别管理奇偶线程等待队列

      • signal() 精准唤醒目标线程。

  4. 实现要点总结

    • 同步机制选择:简单场景用 synchronized,复杂场景用 ReentrantLock

    • 条件判断:必须使用 while 循环检查条件,避免虚假唤醒

    • 数值递增:在同步代码块内完成修改,保证原子性

    • 终止条件:循环外设置退出检测(如 num > 100),防止数值越界

    • 异常处理:捕获 InterruptedException 并确保锁最终释放

  5. 调试与扩展

    • 调试建议

      • 通过 Thread.setName() 设置线程名称,便于日志追踪

      • 使用 jstack 检测死锁,VisualVM 监控线程状态

    • 扩展应用场景

      • 多阶段交替:修改为三个线程交替打印(如 ABCABC 序列)

      • 动态调整上限:通过 volatile 变量实现运行时修改最大值

      • 分布式扩展:结合消息队列(如 Kafka)实现跨进程顺序控制

单例模型

单例模型既然已经用了 synchronized,为什么还要在加 volatile?

  1. 解决指令重排序问题

    • 对象实例化操作 instance = new Singleton() 在 JVM 中分为三步

      • 分配内存空间

      • 调用构造函数初始化对象

      • 将对象引用赋值给变量

    • volatile 时可能发生指令重排序(如顺序变为 ①→③→②),导致其他线程获取未初始化完成的实例

    • volatile 通过内存屏障禁止指令重排序,确保初始化顺序为 ①→②→③,避免「半初始化」对象

  2. 保证可见性

    • 当首个线程完成实例化后,volatile 强制将最新值刷新到主内存,其他线程立即感知 instance 非空,避免重复创建

    • 仅用 synchronized 时,其他线程可能因本地缓存未更新而误判 instance 为空

  3. 双重检查锁定的完整性

    • 第一次判空:减少锁竞争,仅实例未初始化时进入同步块

    • 第二次判空:防止多个线程通过第一次检查后重复初始化

    • volatile 配合:确保第二次判空时读取最新值,避免指令重排序或可见性问题导致判空失效

  4. 性能与安全的平衡

    • synchronized 保证原子性,但无法单独解决指令重排序和可见性问题

    • volatile 以较低性能代价(内存屏障)补充 synchronized 的不足,使 DCL 单例高效且线程安全

线程等待

3 个线程并发执行,1 个线程等待这三个线程全部执行完在执行,怎么实现?

  1. 使用 CountDownLatch

    • 核心机制:创建初始值为 3 的计数器,三个线程执行完毕后调用 countDown() 减少计数

    • 等待线程:通过 await() 阻塞至计数器归零后执行后续任务

  2. 线程 Join 方法

    • 实现方式:在等待线程中依次调用三个并发线程的 join(),使其阻塞直至所有目标线程结束

    • 注意事项

      • 需按顺序先启动并发线程再启动等待线程

      • 适用于简单场景,代码耦合度较高

  3. CompletableFuture 组合

    • 核心方法:通过 CompletableFuture.allOf() 等待所有异步任务完成

    • 优势:支持异步任务组合,可链式调用后续操作(如 thenRun()

  4. 线程池与 awaitTermination

    • 实现流程

      • 将三个任务提交至线程池

      • 调用 shutdown() 关闭线程池后,使用 awaitTermination() 等待任务完成

    • 优势:适合批量任务管理,可设置超时时间防止无限等待

  5. 信号量 (Semaphore)

    • 控制逻辑

      • 初始化三个许可,等待线程需调用 acquire(3) 获取全部许可后执行

      • 每个并发线程执行后调用 release() 释放许可

    • 适用场景:支持动态调整等待线程数量,灵活性较高

  6. 方案选择建议

    • 轻量级场景:优先选择 CountDownLatchCompletableFuture,代码简洁且控制精准

    • 批量任务管理:推荐线程池的 awaitTermination,便于扩展和资源管理

    • 动态调整需求:使用 Semaphore 可灵活控制许可数量

    • 异常处理:所有方案需注意线程泄漏和死锁风险,确保资源释放

并发读写

假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?

  1. 可能的取值范围

    • 最低结果:0(极端情况下所有加操作被覆盖)

    • 最高结果:100(理想线程串行执行)

    • 实际常见值:50~100 之间波动,可能更小(如交替执行多次覆盖修改)

  2. 原因分析

    • 非原子操作n += 1 包含读取、修改、写入三步,可能被线程切换打断

      • 示例:线程 A 读 n=0 → 线程 B 读 n=0 → 均写入 n=1,导致两次操作仅生效一次

    • 缓存可见性问题:多核 CPU 缓存未同步,线程可能读取旧值(如线程 A 修改后未刷回主存,线程 B 仍用 n=0 计算)

    • 指令重排序:编译器或 CPU 调整指令顺序,延迟写入操作加剧数据不一致

  3. 解决方案

    • 同步锁

      • 使用 synchronizedReentrantLock 保证临界区代码原子性

    • 原子变量

      • AtomicInteger 替代普通整型变量,通过 incrementAndGet() 的 CAS 机制保证原子性,性能优于锁

    • 线程安全容器

      • ConcurrentHashMap 提供细粒度并发控制,但本场景更适用原子变量

高级

线程池种类

  1. FixedThreadPool (固定大小线程池)

    • 核心参数corePoolSize = maximumPoolSize,无界队列 LinkedBlockingQueue

    • 特点:线程数固定,适用于任务量稳定场景(如 Web 服务器请求处理)

    • 风险:无界队列可能导致 OOM

  2. CachedThreadPool (可缓存线程池)

    • 核心参数:核心线程数 0,最大线程数 Integer.MAX_VALUE,同步队列 SynchronousQueue

    • 特点:动态扩容线程,空闲线程 60 秒回收(适合短期异步任务)

    • 风险:线程过多可能耗尽资源

  3. SingleThreadExecutor (单线程池)

    • 核心参数:核心线程数 1,无界队列 LinkedBlockingQueue

    • 特点:保证任务顺序执行(如日志处理、数据库操作)

  4. ScheduledThreadPool (定时任务线程池)

    • 核心参数:固定核心线程数,延迟队列 DelayedWorkQueue

    • 特点:支持延时/周期性任务(如定时备份、心跳检测)

  5. WorkStealingPool (工作窃取线程池)

    • 核心参数:基于 ForkJoinPool,默认线程数等于 CPU 核数

    • 特点:任务窃取机制,适合并行计算(如分治算法、图像处理)

  6. 自定义线程池

    • 核心参数:通过 ThreadPoolExecutor 定制核心线程数、队列类型及拒绝策略

    • 适用场景:高并发系统灵活平衡资源与效率

  7. 其他补充

    • ForkJoinPool:专为分治任务设计(如递归计算、大规模数据处理)

    • 拒绝策略:支持 AbortPolicy(抛异常)或 DiscardOldestPolicy(丢弃旧任务)等

线程池参数

  1. corePoolSize (核心线程数)

    • 线程池保持活跃的最小线程数,空闲时不会被销毁(除非设置 allowCoreThreadTimeOut=true

    • 新任务提交时优先创建核心线程处理,直到达到阈值

  2. maximumPoolSize (最大线程数)

    • 允许创建的最大线程总数(核心线程 + 非核心线程)

    • 当队列已满且线程数小于该值时,创建非核心线程

  3. keepAliveTime (空闲线程存活时间)

    • 非核心线程空闲时的最大存活时间,超时后销毁

    • unit 配合使用,默认不影响核心线程

  4. unit (时间单位)

    • 定义 keepAliveTime 的时间单位(如秒、毫秒)

  5. workQueue (任务队列)

    • 存储待执行任务的阻塞队列,影响任务调度逻辑

    • 常见类型

      • LinkedBlockingQueue(无界队列,可能 OOM)

      • ArrayBlockingQueue(有界队列,推荐使用)

      • SynchronousQueue(直接传递任务)

  6. threadFactory (线程工厂)

    • 自定义线程创建方式(命名规则、优先级、守护线程属性)

    • 默认使用 Executors.defaultThreadFactory()

  7. handler (拒绝策略)

    • 线程和队列均满时处理新任务的策略

    • 常用策略

      • AbortPolicy(默认,抛异常)

      • CallerRunsPolicy(提交线程直接执行)

      • DiscardPolicy(静默丢弃)

      • DiscardOldestPolicy(丢弃最旧任务)

  8. 注意事项

    • 队列选择原则

      • CPU 密集型任务推荐有界队列(如 ArrayBlockingQueue

      • I/O 密集型任务可考虑无界队列(需防范 OOM)

    • 参数联动规则

      • 仅当 workQueue 已满且线程数 < maximumPoolSize 时创建非核心线程

      • 使用无界队列时 maximumPoolSize 无效

    • 推荐实践:手动创建 ThreadPoolExecutor 控制队列边界,避免使用 Executors 默认实现

线程池设计

  1. 任务类型决定核心线程数

    • CPU 密集型任务(如加密计算):corePoolSize = CPU 核数 + 1(如 8 核设为 9 线程)

    • I/O 密集型任务(如网络请求):corePoolSize ≈ 2 * CPU 核数(如 8 核设为 16)

    • 混合型任务corePoolSize = Ncpu * Ucpu * (1 + W/C),其中 W 为任务等待时间,C 为计算时间

  2. 队列选择与容量设计

    • 无界队列LinkedBlockingQueue):适用短耗时任务,明确设置容量防止 OOM(如 LinkedBlockingQueue(2000)

    • 有界队列ArrayBlockingQueue):容量计算为 (corePoolSize / 任务平均耗时) * 最大容忍响应时间

    • 同步队列SynchronousQueue):需高实时性,搭配 maximumPoolSize 设为较大值(如 50-100)

  3. 线程池扩容与收缩规则

    • 最大线程数:通常设为核心数的 1.5-3 倍(如电商场景核心 85,最大 150)

    • 空闲线程回收keepAliveTime 设为 30-60 秒,突发流量可延长至 2-5 分钟

    • 动态调整:根据 QPS 监控自动扩容(新核心数 = 当前 QPS * 平均耗时 / 1000 * 0.8

  4. 拒绝策略与容错机制

    • CallerRunsPolicy:允许主线程降级处理(如日志记录)

    • DiscardOldestPolicy:新任务优先级高(如实时消息覆盖)

    • 自定义策略

      • 持久化存储:任务存数据库/Redis 后续处理

      • 异步重试:延迟后提交或转移至备用线程池

  5. 监控与调优实践

    • 监控指标:活跃线程数、队列堆积量、任务平均耗时(推荐 Prometheus 实时监控)

    • 压测验证:某电商优化后 QPS 从 4200 提升至 8500,错误率从 15% 降至 0.2%

    • 弹性设计:容器水平扩容(如 K8s)与线程池参数调优结合应对突发流量

  6. 避坑指南

    • 避免 corePoolSize = maximumPoolSize(丧失突发能力)

    • 明确队列容量(禁用默认无界队列)

    • CPU 密集型任务勿用 CallerRunsPolicy(可能阻塞主线程)

工作原理

  1. 核心组成与初始化

    • 线程池管理器:负责创建、销毁线程及任务调度,维护线程池生命周期

    • 任务队列:存储待执行任务,常用阻塞队列(如 LinkedBlockingQueue),队列满时触发拒绝策略

    • 工作线程:核心线程常驻处理任务,非核心线程队列满时动态创建,空闲超时后销毁

    • 线程工厂与拒绝策略:自定义线程属性(如命名规则),定义队列满时的处理逻辑(如抛异常)

  2. 任务处理流程

    • 任务提交:通过 submit()execute() 提交 Runnable/Callable 任务

    • 资源分配

      • 线程数 < 核心线程数:直接创建新线程执行

      • 核心线程已满:任务入队等待

      • 队列满且线程数 < 最大线程数:创建救急线程处理

      • 队列和线程均满:触发拒绝策略(如 AbortPolicy

    • 任务执行:工作线程从队列取任务执行,完成后等待新任务

  3. 线程池状态管理

    • 状态标识:通过 ctl 变量(高3位状态,低29位线程数)维护:

      • RUNNING:正常接收并执行任务

      • SHUTDOWN:不再接收新任务,处理存量任务

      • STOP:中断所有任务,丢弃未处理任务

      • TERMINATED:线程池完全终止

    • 状态转换shutdown() 进入 SHUTDOWN,shutdownNow() 进入 STOP,完成后转为 TERMINATED

  4. 性能优化机制

    • 线程复用:避免频繁创建/销毁,减少上下文切换(每次 1-10 微秒)

    • 动态伸缩:根据任务量调整救急线程数量,空闲超时后回收

    • 队列选择

      • CPU 密集型:有界队列(如 ArrayBlockingQueue

      • I/O 密集型:无界队列(如 LinkedBlockingQueue

    • 拒绝策略适配:电商系统采用 CallerRunsPolicy 由提交线程执行任务

  5. 典型应用场景

    • Web 服务器:Tomcat 用固定核心线程数(如200)处理 HTTP 请求

    • 异步任务处理:日志写入用缓存线程池(CachedThreadPool)动态扩容

    • 定时任务调度ScheduledThreadPool 支持延迟/周期性任务(如数据同步)

    • 数据库连接池:复用连接减少建立开销(类比线程池设计)。

等待队列

为啥 Java 线程池核心线程满后,入队列等待,Tomcat 核心满后不入队列等待的原因

  1. 设计目标差异

    • Java 线程池:面向 CPU 密集型任务,优先用队列缓冲任务减少线程创建开销。核心线程满后任务入队,队列满时才创建非核心线程。

    • Tomcat 线程池:针对 I/O 密集型场景优化,优先创建新线程降低延迟,核心满后直接扩容线程至最大值,队列仅作最后缓冲

  2. 队列行为实现机制

    • Java 默认队列:使用 LinkedBlockingQueue,核心线程满后直接入队,无界队列可能导致 OOM

    • Tomcat 自定义队列

      • 重写 offer() 方法:当已提交任务数超过活跃线程数时返回 false 触发创建新线程

      • 仅在线程达最大值或队列满时才真正入队

  3. 线程创建策略对比

    • Java 线程池:严格遵循「核心线程→队列→非核心线程」流程,队列未满不扩容

    • Tomcat 线程池:核心线程满后,若提交任务数大于当前线程数,立即创建新线程直至最大值

  4. 性能与资源权衡

    • Java 队列优先:适合耗时长的 CPU 运算任务,减少线程切换损耗

    • Tomcat 线程优先:避免队列延迟,快速响应突发流量(如秒杀请求),默认限制队列长度(acceptCount=100)防 OOM

  5. 参数配置体现差异

    • Java 参数corePoolSizemaximumPoolSize 分离,依赖队列容量控制资源

    • Tomcat 参数

      • maxThreads 限制线程总数,acceptCount 限制队列容量

      • minSpareThreads 预创建核心线程,减少首次请求延迟

内置线程池

为什么不推荐用内置线程池

  1. 资源耗尽风险

    • 无界队列导致 OOM

      • FixedThreadPoolSingleThreadExecutor 使用 LinkedBlockingQueue,任务提交速度过快时队列无限堆积可能引发内存溢出。

      • ScheduledThreadPool 的延迟队列容量为 Integer.MAX_VALUE,周期性任务堆积存在 OOM 隐患

    • 无界线程池问题

      • CachedThreadPool 允许创建 Integer.MAX_VALUE 线程,高并发下可能耗尽 CPU/内存资源

  2. 配置灵活性与容错能力不足

    • 参数固定化

      • 内置线程池的队列类型、核心线程数等参数无法动态调整,难以适配突发流量或资源敏感型任务

    • 默认拒绝策略单一

      • 默认使用 AbortPolicy(抛异常),缺乏降级、重试等容错机制,可能引发服务雪崩

  3. 可维护性与监控缺陷

    • 线程命名模糊

      • 内置线程池生成的线程名称(如 pool-1-thread-n)无业务含义,增加排查难度

    • 缺乏监控支持

      • 无法直接获取活跃线程数、队列堆积量等关键指标,需额外开发监控逻辑

  4. 生产环境最佳实践

    • 推荐替代方案

      • 使用 ThreadPoolExecutor 手动配置:

        • 有界队列:如 ArrayBlockingQueue 控制内存风险

        • 合理线程数:根据 CPU 核数和任务类型(CPU/I/O 密集型)设置 corePoolSizemaximumPoolSize

        • 自定义拒绝策略:结合 CallerRunsPolicy 或降级逻辑(如任务存入外部队列)

    • 线程工厂与命名

      • 自定义 ThreadFactory 为线程添加业务标识,便于日志追踪

    • 生命周期管理

      • 通过 shutdown()shutdownNow() 组合实现优雅关闭,避免线程泄漏

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区