文章目录
问题: Java并发编程中,如何保证操作的原子性?
答:
在Java并发编程中保证操作的原子性的方法:

1、synchronized关键字: 通过同步代码块或方法来保证操作的原子性。
2、Lock接口: 使用ReentrantLock等锁实现提供的锁机制。
3、原子变量: 使用java.util.concurrent.atomic包中的原子变量类。
零、引入
“财务又来找茬了!100 个用户充值,系统少了 800 块!” 王二的吼声差点掀翻技术部天花板,他负责的会员储值系统刚上线,就爆出资金漏洞。领导把流水表拍在他桌上,指关节敲得桌面砰砰响:“今天下班前搞定,不然这个月绩效别要了!”
王二扒着代码急得冒冷汗,核心逻辑就一行balance += 100,本地测着分毫不差,一到线上并发就 “缺斤少两”。隔壁工位的哇哥叼着保温杯凑过来,扫了眼屏幕就笑了:“这是原子性的坑,你以为代码是一步,JVM 早给你拆成三块了。别说绩效,搞不好得自己赔这笔钱。”
点赞 + 关注,跟着哇哥和王二,用食堂打饭的例子搞懂原子性,3 套可直接抄的代码方案,下次面试被问,你能把面试官讲服!

一、破案!1 行代码 = 3 步操作,原子性被撕成了碎片
王二的储值系统逻辑很简单:用户充值 100 元,余额就加 100。可 100 个用户同时充,预期余额该多 10000,实际却只多了 9200,800 块凭空 “消失” 了。
📢 王二的 “裸奔” 代码(必踩坑版)

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 会员储值系统——原子性漏洞版
public class AtomicityBugDemo {
// 初始余额100元
static int balance = 100;
// 100个并发充值任务,每个充100元
static final int TASK_COUNT = 100;
// 等待所有线程执行完
static CountDownLatch latch = new CountDownLatch(TASK_COUNT);
public static void main(String[] args) throws InterruptedException {
// 10个线程的线程池,模拟线上并发
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < TASK_COUNT; i++) {
pool.submit(() -> {
try {
// 核心逻辑:余额加100(看似没问题,实则巨坑)
balance += 100;
} finally {
latch.countDown();
}
});
}
// 等待所有充值完成
latch.await();
pool.shutdown();
System.out.println("实际余额:" + balance); // 线上大概率是9300之类的错误值
System.out.println("预期余额:10100");
}
}
➡️ 哇哥拆穿真相:原子性就是 “不能拆的操作”

“你把balance += 100当‘一口闷’,JVM 却把它拆成了‘读 - 改 - 写’三步曲,” 哇哥拿笔在草稿纸上画,边画边说:
- 读:线程 A 从主内存把 balance=100 读到自己的 “小本本”(工作内存);
- 改:A 在小本本上把 100 改成 200;
- 写:A 把 200 写回主内存;
“这三步中间只要被其他线程插一脚,就出乱子,” 哇哥举了个场景:
- 线程 A 刚读完 balance=100,还没改,线程 B 也读走 100;
- A 改完 200 写回主内存;
- B 不管主内存已经变了,照样把自己的 200 写回去;
- 两次充值,余额只加了 100—— 这 800 块就是这么 “丢” 的!
🎸 用食堂打饭理解原子性

“原子性就像你去食堂打饭,‘刷 10 块钱 + 拿一份套餐’必须是一个整体,” 哇哥打比方,“不能刷了钱没拿到饭,也不能拿了饭没刷钱。代码里的原子操作,就是 CPU 不会在中间切到其他线程,要么完整执行,要么干脆不执行。”
二、方案一:synchronized—— 给代码上 “独占锁”,新手也能上手
“最简单的办法,就是给原子操作加把‘独占锁’,” 哇哥指着代码,“synchronized 就像食堂的‘VIP 打饭窗口’,只有一个线程能进去操作,其他线程都得排队,三步操作就不会被打断了。”
修复后的 synchronized 版代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AtomicityWithSync {
static int balance = 100;
static final int TASK_COUNT = 100;
static CountDownLatch latch = new CountDownLatch(TASK_COUNT);
// 锁对象:所有线程必须抢同一把锁才管用,不能每个线程建一个新对象
static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < TASK_COUNT; i++) {
pool.submit(() -> {
try {
// 关键:把原子操作包在synchronized里,独占执行
synchronized (LOCK) {
balance += 100;
}
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("实际余额:" + balance); // 必为10100,完美对得上
System.out.println("预期余额:10100");
}
}
🥰 哇哥划重点:synchronized 的 “傻瓜式优势”
“synchronized 不用你管锁的释放,就算抛异常,JVM 也会自动把锁放了,” 哇哥补充,“就像你用完 VIP 窗口,门会自动锁上,不用手动关门。适合新手,或者简单的原子操作,比如单个变量增减、简单业务逻辑。”
王二跑起代码,看着控制台输出 10100,长舒一口气:“终于对了!那为什么不所有场景都用它?” 哇哥笑了:“你试试如果业务需要‘抢锁超过 3 秒就放弃’,synchronized 就抓瞎了 —— 这时候得用更灵活的 Lock。”
三、方案二:Lock—— 可超时、可中断的 “智能锁”
果然,没过半天,王二又愁眉苦脸地来找哇哥:“支付场景里,线程全堵在 synchronized 上,导致用户下单超时,客服电话被打爆了!”

📲 用 ReentrantLock 修复的代码(支持超时抢锁)
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AtomicityWithLock {
static int balance = 100;
static final int TASK_COUNT = 100;
static CountDownLatch latch = new CountDownLatch(TASK_COUNT);
// 可重入锁:最常用的Lock实现,和synchronized一样支持重入
static Lock smartLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < TASK_COUNT; i++) {
int userId = i;
pool.submit(() -> {
boolean locked = false;
try {
// 核心:尝试抢锁,3秒没抢到就放弃,不死等
locked = smartLock.tryLock(3, TimeUnit.SECONDS);
if (locked) {
balance += 100;
System.out.println("用户" + userId + "充值成功,余额:" + balance);
} else {
// 抢锁失败,执行降级逻辑,比如返回“系统繁忙,请稍后再试”
System.out.println("用户" + userId + "充值失败:系统繁忙");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("用户" + userId + "充值中断");
} finally {
// 必须手动释放锁!放在finally里,防止异常导致锁泄露
if (locked) {
smartLock.unlock();
}
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("最终余额:" + balance);
}
}
王二拍腿叫好:这功能太刚需了!
“这样就算线程抢不到锁,也不会一直堵着,直接返回系统繁忙,用户体验好多了,” 王二眼睛亮了,“那 Lock 还有啥优势?”
“还能中断等待的线程,” 哇哥补充,“比如系统要停机维护,能把堵在锁上的线程‘喊醒’,让它们赶紧结束,不用等锁释放。但记住,Lock 必须手动解锁,不然锁会一直占着,其他线程全进不来 —— 这是新手最容易踩的坑。”
💯 适用场景:复杂业务的 “救星”
哇哥总结:“如果你的业务需要超时控制、尝试加锁、公平锁(按排队顺序抢锁),或者需要中断等待线程,就用 Lock。比如支付、下单这些核心场景,灵活度比 synchronized 高太多。”
四、方案三:原子类 —— 无锁高效的 “终极杀招”

“那如果只是简单的变量增减,比如库存、计数器,用 Lock 是不是有点小题大做?” 王二举一反三。
“问得好!” 哇哥点赞,“Java 给简单变量准备了‘专用工具’—— 原子类,底层用 CAS 实现,不用加锁也能保证原子性,效率比锁高 3 倍都不止,就像食堂的‘自动打卡机’,不用人工守着,全自动化。”
‼️ 用 AtomicInteger 修复的代码(无锁版)
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicityWithAtomic {
// 原子类:替代普通int,自带原子操作,线程安全
static AtomicInteger balance = new AtomicInteger(100);
static final int TASK_COUNT = 100;
static CountDownLatch latch = new CountDownLatch(TASK_COUNT);
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < TASK_COUNT; i++) {
pool.submit(() -> {
try {
// 原子操作:balance += 100,底层CAS保证不会被打断
balance.addAndGet(100);
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("实际余额:" + balance.get()); // 必为10100
System.out.println("预期余额:10100");
}
}
✅ 用 “打卡机” 讲透 CAS 原理
“原子类的核心是 CAS(比较并交换),就像食堂的自动打卡机,” 哇哥比喻:
- 打卡机先记着你上次的余额(旧值,比如 100);
- 你要充值 100,打卡机会先查现在的余额是不是还 100(确认没被别人改);
- 如果是,就改成 200;如果不是,就重新查最新余额,再试一次;
- 整个过程不用人工干预,也不用锁,效率特别高。
🚀 王二的最终方案

结合储值系统的场景,王二最终用了 “原子类 + Lock” 的组合:
- 余额增减用 AtomicInteger,保证高效无锁;
- 整个支付流程(查余额→扣钱→写日志)用 Lock 加锁,保证复杂逻辑的原子性。
// 支付核心逻辑:复杂业务的原子性保证
public void pay(AtomicInteger balance, int amount, Lock lock) {
boolean locked = false;
try {
locked = lock.tryLock(3, TimeUnit.SECONDS);
if (locked) {
int current = balance.get();
if (current < amount) {
throw new RuntimeException("余额不足");
}
balance.addAndGet(-amount); // 原子扣钱
logService.recordPayLog(); // 写日志,和扣钱在同一锁内
}
} finally {
if (locked) lock.unlock();
}
}
五、总结:保证原子性的 “核心心法”
哇哥拍了拍王二的肩膀,把核心要点缩成三句 “顺口溜”,记牢就不会踩坑:
- 原子操作别拆分:“读 - 改 - 写” 必须捆成一团,不能被线程打断;
- 简单用锁选 sync:新手、简单场景直接上 synchronized,省事儿;
- 复杂场景用 Lock,简单变量用原子,按需选型不盲从。
六、最后说句实在的

原子性是 Java 并发的 “基础防线”,你踩过的金额错乱、库存超卖坑,本质都是没守住这道防线。记住:单线程看逻辑,多线程看原子性,把关键操作做成 “不可拆分” 的原子操作,并发问题就解决了一大半。
如果这篇救了你的项目,点赞 + 分享给你那还在写 “裸奔” 并发代码的同事!

被折叠的 条评论
为什么被折叠?



