读完这篇,你能做什么
能解释更新为何丢失,并根据业务状态必须满足的条件(不变量)选择合适的同步范围和机制。
学习理由、准备要求与知识点
为什么学它
Web 请求共享服务对象,必须识别复合操作与线程隔离的真实边界。
先读这些内容
知识点清单
- count++ 复合操作
- 原子性与可见性
- volatile 的限制
AtomicInteger原子更新- 多字段不变量
Thread.joinThreadLocal生命周期- 单进程锁与跨实例边界
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
两个线程同时改一个对象时,要分别检查三件事:操作会不会互相穿插,写入能否被另一个线程看见,以及哪些先后关系有保证。这分别叫原子性、可见性和顺序。volatile 能提供特定的可见性与顺序保证,但不能把“读取、加一、写回”自动变成一个不可拆开的操作。
例如两个线程同时执行 count++,都可能读到 5,随后都写回 6,结果少加了一次。用 synchronized 可以让同一时刻只有一个线程进入受同一把锁保护的代码,相关访问都要遵守这套用锁规则;AtomicInteger 则提供加一等专门的原子操作。若要同时保证“库存减少”和“已售数量增加”相互对应,仅保护某一个数字还不够,要让相关字段共同遵守业务规则。
让线程各自处理自己的数据,比让它们到处修改同一批字段更容易检查。需要交给其他线程的数据,可以优先考虑创建后不再变化的对象,并约定由谁负责修改。final 只固定引用,不能冻结对象;一个普通集合可以被多个线程读取,也不表示它允许别人同时修改。
把对象交给另一个线程时,还要确保它能看见已经完成的初始化,这属于安全发布问题。线程启动、任务提交、锁和原子变量各有明确规则,不能只凭“我的机器上一直正常”判断。
文中这几个词,先认一认
- 原子性
- 对其他线程而言,相应状态像是一次完整地改变,不会暴露这次操作的中间结果;这不表示其他线程在此期间都停止执行。
- 可见性
- 一个线程写入后,另一个线程在什么条件下能读到这个结果。
- CAS
- 比较后再修改:只有当前值仍等于预期值才写入新值,比较与写入作为一个原子操作完成。
- 线性化点
- 一个并发操作可以被看作在某个瞬间完成;这个瞬间就是它的线性化点。
02看看它怎么工作
CAS 可以理解为“核对旧值后再替换”:先读库存,算出扣减后的数,再要求原子变量只在当前值仍等于刚才读到的值时写入。比较和替换由一次原子操作完成。若比较不相等,说明这次依据已经不适用,要重新读取库存、检查是否够用,再计算新结果。不能拿着旧结果一直重试。本例中,CAS 成功的那一刻可以视为这次扣减真正生效的时刻,术语叫线性化点。
原子变量只保护它所承诺的单个操作。先 get 判断,再单独 set 写入仍可能竞争;两个 AtomicInteger 分别更新也不会自动构成账户转账事务。跨多个字段的不变量可能更适合锁、不可变状态整体替换或更高层事务。把类名带 Atomic 当作整段业务正确保证,是常见误区。
测试应同时检查操作返回值和最终状态。并发执行后库存不为负只是一个性质,还要确认成功次数与实际扣减守恒。Thread.join 等待工作线程结束后再读取结果,避免主线程提前断言。有限自测可以发现常见错误,却不证明所有调度、平台和竞争程度下都已形式化正确,推理仍需依赖原子 API 契约。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["读取当前库存 observed"] --> B{"库存是否足够"}
B -->|"否"| C["返回失败,不修改"]
B -->|"是"| D["计算 next=observed-amount"]
D --> E{"CAS 仍匹配 observed"}
E -->|"成功"| F["返回成功"]
E -->|"失败"| A按顺序看,每一步做了什么
- 写出共享状态和必须保持的不变量。
- 判断需要单值原子操作还是多字段临界区。
- 明确状态发布与读取的可见性。
- CAS 失败时重读并重算。
- 结合守恒断言与竞争测试验证。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/util/concurrent/atomic/AtomicInteger.java · java.util.concurrent.atomic.AtomicInteger#incrementAndGet()
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
import java.util.concurrent.atomic.AtomicInteger;
public final class SourceModel {
public static int increment(AtomicInteger value) {
for (;;) {
int observed = value.get();
int next = observed + 1; // 与 int 原子加一一样,数值范围仍可能环绕。
if (value.compareAndSet(observed,next)) return next;
// 失败说明状态被改变,必须重读,不能复用旧候选。
}
}
}真实 AtomicInteger.incrementAndGet 委托 JDK 内部原子加法能力,再把返回的旧值转换成新值。Java 文件并未用这段 CAS 循环逐行实现全部机器操作。模型使用公开 compareAndSet 展示等价的单值原子更新思路,不声称复制底层指令或虚拟机内建实现。
原子方法仍遵循 int 数值范围,增加到最大值之后可能环绕,并不会自动抛算术溢出。原子性解决并发不可分割,不解决业务数值是否合法。库存算法通过非负状态与足够条件控制减法,另一个计数业务若不允许溢出,需要额外范围规则。
CAS 检查的是观察到的值,不记录完整历史。如果状态先变化又回到相同值,单纯值比较未必能识别中间经历,这就是某些结构需要版本标记的原因。本例只有减法且不暴露任意写入口,状态单调减少,问题范围比一般无锁链表小得多。
有竞争时,CAS 循环可能失败很多次。原子更新不保证每个线程公平得到机会,也不能保证一次调用最多等多久。模型展示的是正确重试方式,实际速度仍受竞争、硬件和调度影响;需要长时间等待或协调多个条件时,锁与条件队列可能更容易写清楚。
代码先调用谁,再调用谁
incrementAndGet请求原子更新- JDK 内部原子加法返回旧值
- Java 层形成新值返回
- CAS 模型通过比较交换重试表达同一单值更新语义
04动手写一个例子
并发扣减有限预算且不出现负数
Budget 初始非负,tryTake(amount) 只接受正数;余额足够时原子扣减并返回 true,否则返回 false。预算只允许扣减,没有补充操作,不暴露底层原子变量。
- 输入 1
初始5,取3再取3- 输出 1
true、false,剩2
- 输入 2
初始0,取1- 输出 2
false
- 输入 3
初始1000,多线程总计尝试1600次取1- 输出 3
恰好1000次成功,剩0
把余额放进私有 AtomicInteger,构造器拒绝负初值,方法拒绝非正扣减。每轮读取 current 后,若小于 amount 就返回失败;否则 candidate=current-amount 非负且在 int 范围内,可以安全尝试 CAS。底层变量不对外暴露,调用者不能绕过规则随意写负数。
CAS 成功表示从观察值到候选值的状态变化是原子的,本次操作返回成功。失败说明另一个线程抢先修改,必须回到读取与余额检查。只允许扣减使余额单调不增,因此读取到不足时可以在线性化于该读取的意义下返回失败。
并发测试让多个线程竞争同一个预算,成功次数用独立原子计数记录。等待全部线程结束后断言剩余为零且成功次数等于初始预算,两个性质共同检查没有超扣或丢失扣减。测试不比较线程完成顺序,也不宣称公平性,因为接口没有这种保证。
为什么这样写是对的余额始终非负,初始余额等于当前余额加所有成功扣减金额之和。
每轮执行一次读取、比较、减法和 CAS,成功或余额不足时结束;争用可能使同一调用多次重试,不能简单写每次操作最坏 O(1)。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
static final class Budget {
private final AtomicInteger remaining;
Budget(int initial) {
if (initial < 0) throw new IllegalArgumentException("initial");
remaining = new AtomicInteger(initial);
}
boolean tryTake(int amount) {
if (amount <= 0) throw new IllegalArgumentException("amount");
for (;;) {
int observed = remaining.get();
if (observed < amount) return false;
// 只有余额足够才形成非负候选,失败后回到顶部重读。
if (remaining.compareAndSet(observed,observed-amount)) return true;
}
}
int remaining() { return remaining.get(); }
}
static void require(boolean condition) {
if (!condition) throw new AssertionError();
}
public static void main(String[] args) throws InterruptedException {
Budget small = new Budget(5);
require(small.tryTake(3) && !small.tryTake(3) && small.remaining() == 2);
require(!new Budget(0).tryTake(1));
boolean zeroRejected = false;
try { small.tryTake(0); } catch (IllegalArgumentException e) { zeroRejected = true; }
require(zeroRejected);
boolean negativeRejected = false;
try { new Budget(-1); } catch (IllegalArgumentException e) { negativeRejected = true; }
require(negativeRejected);
Budget shared = new Budget(1000);
AtomicInteger successes = new AtomicInteger();
Thread[] workers = new Thread[8];
for (int i = 0; i < workers.length; i++) {
workers[i] = new Thread(() -> {
for (int j = 0; j < 200; j++)
if (shared.tryTake(1)) successes.incrementAndGet();
});
workers[i].start();
}
// 所有工作线程结束后再验证守恒,不用 sleep 猜测完成。
for (Thread worker : workers) worker.join();
require(shared.remaining() == 0);
require(successes.get() == 1000); // 成功次数与实际扣减一起检查。
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
- 初值零合法但不能成功扣减。
- 扣减量零或负数拒绝。
- 余额不足不改变状态。
- 多个线程的成功扣减总额不会超过初值。
进阶选读 · 可选深入:多资源原子预留
场景一次操作同时扣多个商品库存或多个账户余额。
做法把需要共同变化的状态放入同一受控事务或锁边界,或以不可变复合状态整体 CAS;失败补偿和持久化策略单独设计。
复杂度成本取决于资源数、争用和事务系统,不能由单个 AtomicInteger 推断。
好处与代价整体原子性更强但竞争范围和失败恢复成本增加,多个独立原子变量不自动组成事务。
05项目里怎么用
优先把共享状态限制在少数清晰组件中,让其它代码使用不可变结果。对请求私有数据不要放进 static 或共享单例字段;对共享缓存也应说明哪些操作需要原子组合。并发问题往往跨写入、读取和生命周期整条链,局部加 volatile 可能只处理了其中一部分。
计数工具要按用途选。AtomicInteger 适合对一个值做准确更新和条件判断;高并发统计还有其他累加结构,但它们读取统计结果的规则,不一定适合库存“够才扣”的判断。不能只因为吞吐更高就直接替换,必须同时检查返回结果和更新保证是否满足业务。
验证并发代码时,要同时检查业务规则、测试和运行记录。一次自测通过,只说明这次实际发生的执行顺序符合断言,不代表所有调度都已覆盖;压测还要记录环境和负载。观察锁等待、重试次数和最慢一批请求的延迟能帮助找到竞争,但优化前先确认任何失败都不会破坏库存、金额等基本规则。
容易踩的坑
- 给计数器加 volatile 就认为 count++ 原子。
- CAS 失败后继续使用旧观察值和候选。
- 多个
AtomicInteger更新被误当成整体事务。 - 用 sleep 等待测试线程并提前读取结果。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 线程内或不可变状态 | 共享推理负担小 | 跨任务协作需传递新值 | 能分离状态的业务 |
| 锁保护临界区 | 多字段不变量容易表达 | 竞争与阻塞需管理 | 复杂复合操作 |
| CAS 原子状态 | 单值条件更新直接 | 重试、公平性和组合复杂度较高 | 短小单状态更新 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01volatile 为什么不能自动保证加一正确?
展开参考答案与推导
加一包含读取、计算和写回多个步骤,volatile 不把它们合成不可分割操作。两个线程仍可能基于同一旧值更新,需要原子方法或覆盖整组动作的锁。
02CAS 成功在本算法中意味着什么?
展开参考答案与推导
它证明余额仍等于本轮观察值,并已原子替换为扣减后的候选。这个交换是成功操作的生效点;失败时必须重读并重新判断余额,不能沿用旧条件。
03原子性为什么不等于不会溢出?
展开参考答案与推导
原子只描述并发状态变化是否不可分割,不扩大数值范围。AtomicInteger 加法仍采用 int 规则,业务不允许环绕时需要范围检查或更合适表示。
换个情况,还会处理吗
01两个账户都用原子变量,转账为何仍可能不一致?
展开参考答案与推导
扣款与入账是两个独立操作,中间失败或并发观察可能看到部分状态。需要共同事务、锁或明确补偿协议,不能由各字段原子推出整个转账原子。
02自测偶尔看到预算尚未用完,先检查什么?
展开参考答案与推导
确认主线程是否等待所有工作线程真正结束、工作线程是否异常退出,以及总尝试次数是否足够。先修复测试同步与输入前提,再判断预算算法,不应靠增加 sleep 时间掩盖。
03CAS 重试很高是否一定要继续加线程?
展开参考答案与推导
高重试可能说明争用已经成为瓶颈,更多线程可能增加无效工作。观察重试和尾延迟,考虑减少共享、分批或锁方案;选择应基于语义与负载而非无锁标签。
写代码试一试
01验证一次取尽预算
- 输入 / 条件
初始7,取7再取1- 输出 / 预期
true、false,剩0
提示 等于余额也是合法成功。
展开参考答案与推导
加入精确取尽测试,检查条件使用 observed < amount 而不是小于等于。后续取一失败且余额不变,覆盖从正数到零的边界。
02增加成功扣减金额统计
- 输入 / 条件
不同线程分别取不同正数量- 输出 / 预期
统计总额加剩余额等于初值
提示 成功后累计金额而非次数。
展开参考答案与推导
在测试中用 AtomicLong 累计每次成功的 amount,全部线程 join 后验证守恒。金额不同时时间上成功次数不再等于扣减总额,测试指标必须对应业务不变量。
03为预算增加补充操作
- 输入 / 条件
初始0,补5后取3- 输出 / 预期
剩2
提示 补充也需要原子范围检查。
展开参考答案与推导
补充方法使用 CAS 循环和精确加法验证上界,不能直接 set(get()+amount)。同时重新审查单调减少前提和失败返回解释,增加与扣减交错的测试,而不只复制原方法。
读完之后,独立完成
课后作业:自己做一遍
本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能解释更新为何丢失,并根据业务状态必须满足的条件(不变量)选择合适的同步范围和机制。
本篇重点加练
重点强化可见性、多字段不变量和线程复用中的生命周期,避免只验证最终计数。
- 原子性与可见性
- 多字段不变量
ThreadLocal生命周期
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
初值 count=4,两个线程各执行一次 count++。展开关键动作并列出多种合法交错,分别标出读取、计算、写入;说明仅运行一次程序能支持哪些结论。
- 提交内容
- 交错时间线与证据边界说明。
- 怎样算完成
- 交错符合各线程程序顺序
- 区别原子性与可见性
练到的知识点count++ 复合操作 · 原子性与可见性
- 作业 02排错实验
分别用普通
int、volatileint与AtomicInteger实现两个线程累计任务;控制启动和等待完成,重复有限次数,记录观察结果并解释哪些性质不能只靠压力测试证明。- 提交内容
- Java 实验、自写总量检查、运行条件及记录。
- 怎样算完成
- 实验规模有界
- 每次都等待工作结束
- 区分观测与机制论证
练到的知识点volatile 的限制 · AtomicInteger 原子更新
- 作业 03原理问答
同一个发布标志与数据字段分别被不同线程使用。画出可能的读写关系,并提出需要什么同步合同才能让读取方合理使用数据;不能以执行速度快慢代替依据。
- 提交内容
- 访问图与同步条件说明。
- 怎样算完成
- 可见性问题有具体对象与字段
- 所需条件能转成测试或机制论证
练到的知识点原子性与可见性
综合应用
- 作业 04设计评审
库存状态包含 available、reserved、sold,初始为 (20,0,0),三个操作可并发改变状态。先定义业务状态必须遵守的条件(不变量),再设计何时允许读取一致快照及何时判定工作全部完成。
- 提交内容
- 状态转换规则、同步范围与验收案例。
- 怎样算完成
- 涵盖多字段整体状态
- 完成判定与业务一致性分开
练到的知识点多字段不变量 · Thread.join
- 作业 05面试场景
固定线程池处理请求 A 后再处理 B,身份存在线程本地变量;服务部署两个进程,各自使用 synchronized 控制全站名额。分别列出两种设计需要验证的作用范围。
- 提交内容
- 两张作用域图、故障复现场景与修复验收条件。
- 怎样算完成
- 覆盖线程复用和异常路径
- 区分线程、进程与服务范围
练到的知识点ThreadLocal 生命周期 · 单进程锁与跨实例边界
- 作业 06编程实践
实现一次最多领取三张的单进程票箱,初始票数 17;领取失败不改变状态,每次成功返回领取数。用多个线程执行有界请求并核对最终账本,不以打印顺序判定正确性。
- 提交内容
- Java 实现、自写零票/超额/并发测试和运行证据。
- 怎样算完成
- 总量守恒有断言
- 工作结束后再汇总
- 说明成功操作的可观察边界
练到的知识点AtomicInteger 原子更新 · 多字段不变量 · Thread.join
- 作业 07排错实验
编写两个线程交换库存“待售/已售”字段的实验,记录单次快照而不仅是最终总和;定义合法快照集合,比较两种同步范围的观测结果。
- 提交内容
- Java 实验、自写合法状态检查、有限重复运行证据。
- 怎样算完成
- 多字段状态有独立断言
- 观察结果与机制论证分开
练到的知识点原子性与可见性 · 多字段不变量
- 作业 08编程实践
实现可复用工作线程的请求上下文包装器,任务可正常返回或抛错,后续任务读取自己的用户标识;自写检查任务前后上下文状态。
- 提交内容
- Java 实现、自写异常/无身份/连续任务测试及运行证据。
- 怎样算完成
- 每次执行生命周期可追踪
- 任务失败不绕过清理验收
练到的知识点ThreadLocal 生命周期
- 作业 09设计评审
请求上下文包含租户标识,库存服务还维护共享的两字段统计。确定哪些数据属于线程、请求和服务,并制定异常及线程切换后的读取合同。
- 提交内容
- 作用域图、所有权表和验收序列。
- 怎样算完成
- 上下文与业务共享状态分别处理
- 跨线程传递有明确责任
练到的知识点多字段不变量 · ThreadLocal 生命周期
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
掌握锁与性能测量后:比较原子重试与互斥锁在同一票箱工作负载中的适用性,制定公平的测量计划;要求同时关注失败重试次数、延迟与业务正确性。
- 提交内容
- 实验设计及选择依据,不要求给出最快实现。
- 怎样算完成
- 相同工作量与预热条件
- 性能判断不牺牲一致性
练到的知识点AtomicInteger 原子更新 · 多字段不变量 · 单进程锁与跨实例边界
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
检验“每个字段都用原子类就能保证任何组合约束”的主张。构造一个至少两个字段的业务状态和合法交错,并说明你的实验能验证或不能验证什么。
- 提交内容
- 业务约束、交错时间线及可选最小实验。
- 怎样算完成
- 反例或证明覆盖整体状态
- 没有把一次压力测试当作完整证明
练到的知识点原子性与可见性 · 多字段不变量
- 作业 12思维拓展 · 条件变化
工作从固定线程池改为可暂停、恢复到其他线程的任务流程,原有请求上下文与统计读取方案哪些前提需要重审?尚未学异步框架时只分析给定线程切换序列。
- 提交内容
- 前提变化表与新的上下文合同。
- 怎样算完成
- 任务身份与线程身份分别说明
- 业务不变量在切换前后可验证
练到的知识点多字段不变量 · ThreadLocal 生命周期
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Java 17 API:AtomicInteger (新标签页)↗
核对单次原子更新和复合业务不变量之间的边界。
- 实践参考文档英文
Java 17 API:ThreadLocal (新标签页)↗
阅读 get
/set/remove的线程绑定规则以及线程池中的清理责任。 - 深入参考文档英文
Microsoft Win32:进程与线程 (新标签页)↗
阅读地址空间、线程调度和共享资源,连接 Java 进程与系统观察工具。
- 实践实践指南英文
Deadlock Empire:亲手决定两个线程谁先走 (新标签页)↗
从入门引导开始,只研究一对线程和一个共享变量,记录一条正常顺序与一条出错顺序;产出两列执行日志。之后再尝试锁相关关卡。游戏采用自己的简化执行规则,不能据此证明 Java 内存可见性、JIT 重排或真实系统的所有线程行为。 免费英文网页游戏,需要
JavaScript。入门示例采用 C# 写法;先做两个线程的逐步执行练习,之后再尝试锁相关关卡。 - 深入参考文档英文
OpenJDK 17u java.util.concurrent.atomic.AtomicInteger (新标签页)↗
定位
AtomicInteger.incrementAndGet的内部原子加法委托,教学模型使用公开 CAS,不冒称底层指令实现。 - 深入参考文档英文
Java17 内存模型规范 (新标签页)↗
阅读线程动作、同步顺序、happens-before 与数据竞争规则。
- 深入参考文档英文
Java17 并发包说明 (新标签页)↗
阅读任务执行、同步工具与内存一致性效果。