Wrayの知识库 Wrayの知识库
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
  • Java章节编写规范
  • Java基础

  • Java集合

  • Java并发

    • Java并发概述
    • 线程与进程
    • Thread类与线程生命周期
    • 线程创建与任务模型
    • 线程安全
    • synchronized关键字
    • volatile关键字
    • Java内存模型(JMM)
    • 线程间通信
    • 线程池
    • 并发工具类
    • 原子操作类Atomic
    • 并发锁
    • 并发容器
    • ConcurrentHashMap
    • BlockingQueue
    • CopyOnWriteArrayList
    • ThreadLocal
    • Fork/Join框架
    • ScheduledThreadPoolExecutor
    • CompletableFuture
    • 虚拟线程
    • 死锁活锁与线程问题排查
    • 并发编程最佳实践
      • 1. 并发设计优先级
      • 2. 优先减少共享状态
      • 3. 线程池必须可治理
      • 4. 锁使用原则
      • 5. 超时、取消与中断
      • 6. 异步编排原则
      • 7. 并发容器选型
      • 8. 监控指标
      • 9. 测试并发代码
      • 10. 代码评审清单
      • 11. 面试表达
      • Tips 快问快答
  • Java IO

  • JVM

目录

并发编程最佳实践

并发编程最佳实践关注的不是“会用多少并发 API”,而是怎样写出可预测、可维护、可观测、出问题后能快速止损的并发代码。

这一节把前面线程、锁、线程池、并发容器、异步编排和虚拟线程等内容收束成生产开发原则。

# 1. 并发设计优先级

写并发代码时,优先级应该是:

减少共享状态
  │
  ▼
明确所有权
  │
  ▼
选择同步工具
  │
  ▼
设置边界与超时
  │
  ▼
监控、压测、复盘

很多并发问题不是因为少用了高级工具,而是因为共享状态太多、生命周期不清、异常和超时没有设计。

# 2. 优先减少共享状态

共享状态越少,需要同步的地方越少。

推荐方式:

做法 说明 示例
局部变量 每个线程独立栈帧 请求内临时计算
不可变对象 创建后不再修改 配置快照、值对象
消息传递 通过队列交接数据 生产者消费者
分片数据 降低热点竞争 分段计数、按用户分桶
线程封闭 数据只被一个线程访问 单线程事件循环

不可变对象示例:

public record PriceRule(String skuId, BigDecimal discount, Instant expireAt) {
}

如果对象创建后状态不变,多个线程共享时就不需要锁来保护内部字段。

可变共享对象错误示例:

class DiscountContext {
    private final Map<String, BigDecimal> cache = new HashMap<>();

    BigDecimal get(String skuId) {
        return cache.computeIfAbsent(skuId, this::loadDiscount);
    }
}

如果这个类被多个线程共享,HashMap 就会有线程安全问题。修复方式可以是使用 ConcurrentHashMap,也可以把缓存所有权限定在单线程或请求上下文中。

# 3. 线程池必须可治理

生产线程池不能只有“能跑”,还要能治理。

线程池设计清单:

项目 要求 原因
命名 线程名包含业务含义 方便日志和 dump 定位
有界 线程数和队列容量有上限 防止资源失控
拒绝策略 拒绝时可观测、可降级 防止静默丢任务
超时 外部调用和 Future 等待要超时 防止无限占用线程
隔离 不同业务或慢任务拆池 避免互相拖垮
监控 活跃线程、队列、拒绝、耗时 提前发现拥塞
关闭 应用退出时优雅关闭 防止任务丢失和进程挂住

推荐模板:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        8,
        16,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1000),
        new NamedThreadFactory("order-check"),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

线程池不是越大越好。线程池大小要和 CPU、下游连接池、数据库容量、业务延迟目标一起看。

# 4. 锁使用原则

锁的目标是保护不变量,而不是把代码块圈起来。

共享数据
  │
  ▼
不变量:余额不能为负、库存不能超卖、状态不能倒退
  │
  ▼
用锁保护“检查 + 修改”的完整临界区

库存扣减示例:

class Inventory {
    private int stock;

    synchronized boolean deduct(int count) {
        if (stock < count) {
            return false;
        }
        stock -= count;
        return true;
    }
}

这里锁保护的是“检查库存是否足够”和“扣减库存”这两个动作的原子性。

锁实践建议:

  • 锁对象必须私有,不要锁字符串常量、包装类或外部可见对象。
  • 多把锁要固定加锁顺序。
  • 锁内不要做慢 IO、RPC、数据库调用。
  • 不要在锁内调用未知外部回调。
  • 锁保护的数据和锁对象要放在同一个类的边界内。
  • 能用不可变对象替代共享可变状态时,优先不可变。

错误示例:

synchronized (userId.intern()) {
    updateUser(userId);
}

intern 会把字符串放入全局字符串池,锁范围可能被意外扩大。更好的做法是使用受控的分段锁或业务级幂等设计。

# 5. 超时、取消与中断

没有超时的并发程序很容易在故障时耗尽资源。

必须设置超时的场景:

  • RPC、HTTP、数据库、缓存调用。
  • Future.get、CompletableFuture.join/get。
  • 队列 offer/poll。
  • 锁获取,例如 tryLock(timeout)。
  • 批处理任务等待子任务完成。

示例:

Future<Order> future = executor.submit(() -> queryOrder(orderId));

try {
    return future.get(800, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
    future.cancel(true);
    throw new OrderQueryTimeoutException(orderId, e);
}

任务取消依赖中断协作。任务代码必须尊重中断。

while (!Thread.currentThread().isInterrupted()) {
    Task task = queue.poll(500, TimeUnit.MILLISECONDS);
    if (task != null) {
        handle(task);
    }
}

不要吞掉中断。

try {
    queue.take();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}

恢复中断标记能让上层逻辑继续感知取消信号。

# 6. 异步编排原则

异步编排的目标是减少阻塞等待,而不是把同步代码包装成异步形式。

推荐:

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> queryUser(userId), userExecutor);

CompletableFuture<List<Order>> orderFuture =
        CompletableFuture.supplyAsync(() -> queryOrders(userId), orderExecutor);

return userFuture
        .thenCombine(orderFuture, UserOrderView::new)
        .orTimeout(1, TimeUnit.SECONDS)
        .exceptionally(this::fallbackView);

不推荐:

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> queryUser(userId), executor);

User user = userFuture.join();

如果立刻 join,异步带来的收益会大幅下降,还可能造成线程池饥饿。

异步代码必须关注:

  • 使用自定义线程池。
  • 每个外部调用有超时。
  • 异常链路有兜底。
  • 结果合并后处理部分失败。
  • ThreadLocal 上下文要显式传递。
  • 不在同一个小线程池里同步等待子任务。

# 7. 并发容器选型

并发容器不是普通容器加锁的唯一替代,也不是万能解法。

场景 推荐 说明
高并发读写 Map ConcurrentHashMap 分散锁竞争,适合缓存和索引
读多写少 List CopyOnWriteArrayList 写时复制,读无锁
生产消费 BlockingQueue 队列自带阻塞协作
计数器 LongAdder、AtomicLong 高竞争计数可优先 LongAdder
有序任务 DelayQueue、优先级队列 注意无界队列风险
不变快照 List.copyOf 等 适合发布只读数据

使用并发容器时仍要注意复合操作。

if (!map.containsKey(key)) {
    map.put(key, value);
}

这不是原子操作。应使用:

map.putIfAbsent(key, value);

或者:

map.computeIfAbsent(key, this::loadValue);

# 8. 监控指标

并发程序一定要可观测。

线程池至少监控:

指标 含义 风险信号
activeCount 正在执行任务的线程数 长期接近 maximumPoolSize
poolSize 当前线程数 异常增长
queueSize 队列长度 持续上升
completedTaskCount 已完成任务数 增长变慢
rejectedCount 拒绝次数 出现即要关注
taskLatency 任务执行耗时 P95/P99 上升
queueWaitTime 排队等待时间 线程池拥塞

除了线程池,还要看:

  • CPU 使用率和负载。
  • GC 停顿和分配速率。
  • 数据库连接池等待时间。
  • HTTP 客户端连接池。
  • 锁等待时间。
  • 接口超时率和错误率。

并发优化必须用指标验证,不能只凭感觉。

# 9. 测试并发代码

并发代码测试要覆盖正确性和压力。

测试类型 目标 示例
单元测试 验证基本逻辑 状态转换、边界条件
多线程测试 暴露竞态 多线程同时扣减库存
压测 观察吞吐和延迟 JMeter、wrk、业务压测
稳定性测试 观察长时间运行 队列是否堆积、内存是否泄漏
故障注入 验证超时和降级 下游变慢、异常、连接耗尽

多线程扣减示例:

int threadCount = 20;
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(threadCount);

for (int i = 0; i < threadCount; i++) {
    executor.execute(() -> {
        await(start);
        inventory.deduct(1);
        done.countDown();
    });
}

start.countDown();
done.await(3, TimeUnit.SECONDS);

CountDownLatch 可以让多个线程尽量同时开始,提高复现竞态条件的概率。

# 10. 代码评审清单

评审并发代码时,可以逐项检查:

  1. 是否存在共享可变状态。
  2. 共享状态是否有明确同步策略。
  3. 锁对象是否私有且稳定。
  4. 多把锁是否固定顺序。
  5. 锁内是否存在慢 IO 或外部调用。
  6. 线程池是否有界、命名、可监控。
  7. 等待 Future、锁、队列、外部调用是否有超时。
  8. 是否正确处理中断和取消。
  9. 异常是否会被吞掉。
  10. 是否存在同池任务互相等待。
  11. 并发容器复合操作是否原子。
  12. 是否有压测或并发测试覆盖。

# 11. 面试表达

如何写好生产级 Java 并发代码?

可以这样回答:

我会先尽量减少共享状态,用不可变对象、局部变量、队列或数据分片降低同步需求。如果必须共享状态,就明确锁保护的不变量,控制锁范围,避免锁内慢 IO,多把锁固定顺序。线程池必须显式配置线程数、队列、线程名、拒绝策略和监控,所有外部调用、Future 等待和锁等待都要有超时。最后通过压测、线程 dump、线程池指标和故障注入验证并发设计是否可靠。

# Tips 快问快答

Q:并发编程最重要的原则是什么? A:优先减少共享可变状态,其次才是选择锁或并发工具。

Q:线程池为什么必须命名? A:线程名能在日志和线程 dump 中直接定位业务来源。

Q:锁保护的到底是什么? A:锁保护的是共享数据的不变量,以及检查和修改之间的原子性。

Q:锁内为什么不能做 RPC 或数据库调用? A:慢调用会放大锁持有时间,导致大量线程阻塞。

Q:为什么要恢复中断标记? A:捕获 InterruptedException 后恢复中断,能让上层代码继续感知取消信号。

Q:并发容器能解决所有线程安全问题吗? A:不能。单个方法通常线程安全,但多个方法组合起来仍可能不是原子的。

Q:异步代码为什么也要超时? A:没有超时的异步任务会长期占用线程、连接和内存。

Q:CompletableFuture 默认线程池能不能用? A:简单非阻塞任务可以,生产阻塞任务建议使用自定义线程池。

Q:如何判断线程池快被打满? A:看活跃线程数、队列长度、排队时间和拒绝次数是否持续上升。

Q:并发测试为什么需要 CountDownLatch? A:它可以让多个线程同时起跑,更容易暴露竞态问题。

上次更新: 2026/06/25, 14:19:18
死锁活锁与线程问题排查
Java IO概述

← 死锁活锁与线程问题排查 Java IO概述→

Copyright © 2023-2026 Wray | 鄂ICP备2024050235号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式