并发编程需要注意哪些问题?
未标记
考查重点:原子性、可见性与资源生命周期。共享读改要原子且可见,依赖执行要保序;设并发上限、超时/取消和资源清理,异步不自动消除竞争
本题目录
参考答案
本答案基于相关资料,由 AI 辅助编写,供学习与面试准备参考。如有疏漏,欢迎反馈指正。
核心回答
并发首先要保证业务语义正确,再讨论吞吐。重点检查共享状态的原子性和可见性、任务之间的依赖顺序、资源上限,以及超时、异常和取消如何传播。把同步调用改成异步,并不会自动使其更快,也不会消除失败。
先划清哪些任务能并行
如果两个查询互不依赖,可以并行后汇总;如果第二步依赖第一步生成的订单号,就不能直接同时执行。共享对象尽量使用不可变数据、任务独有数据或消息传递,必须共享时再选择锁、原子类、并发容器等同步方式。
volatile 可用于特定可见性和有序性需求,但不把“读取、判断、修改”自动合成一个原子操作。并发容器的单个方法安全,也不意味着多个方法组合后仍满足业务约束,例如先查询不存在再插入,仍可能需要原子接口或唯一约束。
改成异步时还要补齐什么
- 顺序与结果:明确汇总何时完成、是否允许部分成功、结果是否按输入顺序排列;不能把提交成功当作业务完成。
- 异常与超时:获取异步结果时处理异常,为整条请求分配总时间预算。等待超时并不保证工作已经停止,取消也往往需要任务配合。
- 资源与背压:线程数、队列长度、在途请求及下游连接都要有界。上游并发增加不会提高数据库本身的容量,无限排队只会把过载变成长延迟和内存占用。
- 上下文与生命周期:线程切换后核对日志追踪、身份、事务边界与
ThreadLocal清理;请求结束后仍运行的任务要有明确归属。 - 共享锁风险:统一加锁顺序,缩小临界区,避免持锁等待远程调用或等待同线程池中的子任务,防止死锁和线程饥饿。
怎样验证改造有效
先确认瓶颈是可重叠的等待,而不是 CPU 已满或下游过载。用相同负载比较响应时间分位数、吞吐、错误率、排队时间、CPU、内存与下游压力,并覆盖任务失败、超时、部分成功和取消。测试中尽量用屏障或受控调度制造竞争条件,单次“没复现”不能证明没有竞态。
对应教程第 24 章的 Send 并行分发与 Reducer 合并可以帮助理解“任务并行、结果汇总”的结构;Java 线程内存语义和具体资源控制还要另外处理。
常见追问
任务都并行后,响应时间就是最慢任务的时间吗? 只在资源足够、依赖可并行等理想条件下近似成立,还要加上排队、调度和汇总成本。
加锁能解决全部问题吗? 不能。锁需要覆盖正确的共享不变量,也必须考虑死锁、锁竞争和锁外的副作用。
参考资料
- 国内:JavaGuide / Snailclimb:Java 内存模型:可见性与 happens-before 部分解释共享变量为何需要明确同步关系,以及 volatile 与互斥的边界。
- 国内:JavaGuide / Snailclimb:Java 线程池详解:借鉴线程池流程,解释正文中异步任务的排队、执行与异常观察。
- 国内:JavaGuide / Snailclimb:Java 线程池最佳实践:有界队列和下游容量部分说明并发预算如何避免排队失控与压力转移。
- 国外:Oracle:JLS SE21 Chapter17:依据语言规范中锁、可见性与内存顺序的规则,说明正文中共享状态正确性的同步依据。
- 国外:Oracle:java.util.concurrent package, SE21:任务提交、Future.get 和并发容器的内存一致性规则解释安全发布与结果汇总的依据。
