并发设计的首选策略是减少共享可变状态。能用不可变值传递、单线程所有权或消息队列解决时,不要先增加锁。必须共享时, 把不变量、同步边界和锁顺序写进设计并用确定时序测试。
Table of contents
从固定锁顺序开始
下载确定性转账样例下载, 运行:
java -cp target/classes dev.elaine.concurrency.DeterministicTransferSample
两个线程同时执行相反方向转账。代码始终按账户 ID 从小到大获取两把锁,因此不会形成 A 等 B、B 等 A 的循环等待;最终
输出 总金额=20000。
四种减少共享的模式
| 模式 | 核心约束 | 适合场景 |
|---|---|---|
| 不可变快照 | 创建后不修改 | 配置、查询结果、事件 |
| 线程封闭 | 状态只由一个线程访问 | 事件循环、批次聚合 |
| Actor 思想 | 每个实体串行处理消息 | 订单、会话、设备状态 |
| 分区所有权 | 按键路由到固定分片 | 高吞吐状态更新 |
Actor 不等于必须引入某个框架。单线程执行器加有界邮箱也能体现所有权,但要设计邮箱容量、失败监督、消息顺序和持久化。
不要用 sleep 构造时序
Thread.sleep(100) 不能证明另一个线程已到达目标位置,只会让测试在快机器上浪费时间、在慢机器上偶发失败。使用
CountDownLatch、Barrier、Phaser 或测试钩子明确控制“已读旧值”“允许继续”等事件。
var ready = new java.util.concurrent.CountDownLatch(2);
var start = new java.util.concurrent.CountDownLatch(1);
// 每个工作线程先 ready.countDown(),再 start.await()
所有等待都应有测试级超时。超时失败时打印线程转储,而不是让 CI 永久挂起。
测试业务不变量
不要断言线程输出顺序,除非顺序本身是契约。并发转账测试应断言总金额守恒、余额不为负、所有任务结束且没有未捕获异常。 对 CAS、队列和锁等底层算法,普通 JUnit 不足以覆盖所有内存交错,可使用 JCStress;性能结论使用 JMH。
线性化点与故障路径
为并发操作指出线性化点:整个操作在逻辑上瞬间生效的位置,例如成功 CAS 或持锁后的状态写入。再逐项测试中断、超时、 拒绝、重复消息、部分失败和关闭过程。
生产验证还需要指标与事件:队列深度、锁等待、任务年龄、拒绝、取消、超时和不变量告警。没有这些证据,并发错误只会表现 为偶发业务数据异常。
下一步
继续阅读网络、I/O 与 Java 服务器阶段,把线程模型、背压和取消协议 应用到 Socket、NIO、Netty 与 HTTP 服务。