核心概念
并发编程的原子性
并发编程中的原子性 是指一个或多个操作在CPU执行过程中不会被线程调度机制打断,这些操作要么全部执行完成,要么全部不执行,不会被其他线程看到中间状态。
关注点:线程调度、指令执行、内存可见性
数据库ACID的原子性
数据库事务的原子性(Atomicity) 是指事务中的所有操作要么全部成功提交,要么全部失败回滚,不会出现部分成功的情况。
关注点:事务完整性、持久化、故障恢复
核心区别
| 维度 | 并发编程原子性 | 数据库ACID原子性 |
|---|---|---|
| 定义层面 | 操作执行层面 | 事务提交层面 |
| 时间粒度 | CPU指令级别(纳秒) | 事务级别(毫秒到秒) |
| 范围 | 单个或少量操作 | 多个SQL操作 |
| 保证方式 | CPU指令、锁、CAS | 日志、锁、MVCC |
| 失败处理 | 重试或异常 | 回滚到事务开始前 |
| 持久化 | 不涉及持久化 | 保证数据持久化 |
| 关注问题 | 数据竞争、可见性 | 数据一致性、故障恢复 |
详细对比
1. 并发编程的原子性
示例1:非原子操作
public class ConcurrencyAtomicity {
private int count = 0;
// ❌ 非原子操作
public void increment() {
count++; // 分解为三个步骤:
// 1. 读取count到寄存器
// 2. 寄存器值+1
// 3. 写回count
}
}
// 执行过程:
// 线程A: 读取(0) → 计算(1) → [被调度] → 写回(1)
// 线程B: 读取(0) → 计算(1) → 写回(1)
// 结果:两次increment,count只增加了1次
问题本质:线程调度可能在任意指令之间发生,导致操作被打断。
示例2:保证原子性
public class AtomicOperation {
private AtomicInteger count = new AtomicInteger(0);
// ✅ 原子操作(通过CAS实现)
public void increment() {
count.incrementAndGet(); // CPU级别的原子指令
}
// 或使用synchronized
private int count2 = 0;
public synchronized void increment2() {
count2++; // 整个方法是原子的
}
}
关键点:
- 硬件层面:CPU提供的原子指令(如
LOCK CMPXCHG) - 软件层面:通过锁保证操作不被打断
- 目标:避免数据竞争,保证中间状态不可见
2. 数据库的原子性
示例1:转账事务
public class DatabaseAtomicity {
// 数据库事务的原子性
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 操作1:扣减账户A
accountDao.deduct(fromId, amount);
// 假设这里发生异常
if (amount.compareTo(new BigDecimal("1000")) > 0) {
throw new RuntimeException("金额过大");
}
// 操作2:增加账户B
accountDao.add(toId, amount);
// 如果任何操作失败,整个事务回滚
// 不会出现A扣款成功但B未收到钱的情况
}
}
关键点:
- 逻辑层面:多个SQL操作作为一个整体
- 持久化保证:提交后数据永久保存,回滚后完全恢复
- 故障恢复:即使系统崩溃,也能通过日志恢复或回滚
- 目标:保证业务逻辑的完整性
示例2:ACID中的原子性
BEGIN TRANSACTION;
-- 操作1
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 操作2
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 如果到这里发生错误
-- 数据库会自动回滚上面的所有操作
COMMIT; -- 或 ROLLBACK
实现机制:
- Undo Log:记录修改前的数据,用于回滚
- Redo Log:记录修改后的数据,用于重做
- 两阶段提交:保证分布式事务的原子性
相同点
1. 都是”全或无”的保证
// 并发编程:CAS操作
AtomicInteger count = new AtomicInteger(0);
count.compareAndSet(0, 1); // 要么成功设置为1,要么失败保持0
// 数据库:事务操作
@Transactional
public void businessLogic() {
// 要么全部SQL成功,要么全部回滚
dao.insert(...);
dao.update(...);
}
2. 都需要隔离其他操作
// 并发编程:synchronized隔离其他线程
public synchronized void method() {
// 这里的操作不会被其他线程干扰
}
// 数据库:事务隔离级别隔离其他事务
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
-- 这里的操作不会受其他事务影响
COMMIT;
3. 都有性能开销
// 并发编程:锁的开销
synchronized (lock) { // 获取锁、释放锁有开销
// ...
}
// 数据库:事务的开销
BEGIN TRANSACTION; // 维护事务状态、日志有开销
// ...
COMMIT;
实际应用对比
场景1:库存扣减
并发编程方式
public class StockService {
private AtomicInteger stock = new AtomicInteger(100);
// 并发编程的原子性:内存级别
public boolean deduct() {
int current;
do {
current = stock.get();
if (current <= 0) {
return false; // 库存不足
}
} while (!stock.compareAndSet(current, current - 1));
return true;
}
}
特点:
- 操作在内存中完成,速度快
- 只保证内存可见性,不保证持久化
- 系统重启后数据丢失
数据库方式
@Service
public class StockService {
@Autowired
private StockDao stockDao;
// 数据库的原子性:事务级别
@Transactional
public boolean deduct(Long productId) {
// 1. 查询库存
Stock stock = stockDao.selectForUpdate(productId);
if (stock.getQuantity() <= 0) {
return false;
}
// 2. 扣减库存
stock.setQuantity(stock.getQuantity() - 1);
stockDao.update(stock);
// 3. 记录扣减日志
stockDao.insertLog(new StockLog(productId, -1));
return true;
// 如果任何操作失败,整个事务回滚
}
}
特点:
- 操作涉及磁盘I/O,速度较慢
- 保证数据持久化
- 可以包含多个复杂操作
- 支持故障恢复
场景2:计数器
并发编程方式
// 高性能计数器
public class Counter {
private LongAdder count = new LongAdder();
public void increment() {
count.increment(); // 原子操作,无锁,极快
}
public long getCount() {
return count.sum();
}
}
// 性能:单机百万级QPS
数据库方式
// 持久化计数器
@Service
public class Counter {
@Autowired
private CounterDao counterDao;
@Transactional
public void increment(String key) {
Counter counter = counterDao.selectByKey(key);
counter.setValue(counter.getValue() + 1);
counterDao.update(counter);
}
}
// 性能:单机数千QPS
// 优点:数据持久化,可跨系统共享
组合使用
在实际项目中,常常需要组合使用两种原子性保证。
示例:秒杀系统
@Service
public class SeckillService {
// 内存中的库存(并发编程原子性)
private AtomicInteger memoryStock = new AtomicInteger(100);
@Autowired
private OrderDao orderDao;
public boolean seckill(Long userId, Long productId) {
// 第一步:内存中快速扣减(并发编程原子性)
int current;
do {
current = memoryStock.get();
if (current <= 0) {
return false; // 库存不足,快速返回
}
} while (!memoryStock.compareAndSet(current, current - 1));
// 第二步:数据库中创建订单(数据库原子性)
try {
createOrder(userId, productId);
return true;
} catch (Exception e) {
// 失败则回补内存库存
memoryStock.incrementAndGet();
return false;
}
}
@Transactional // 数据库事务的原子性
private void createOrder(Long userId, Long productId) {
// 1. 扣减数据库库存
stockDao.deduct(productId, 1);
// 2. 创建订单
Order order = new Order(userId, productId);
orderDao.insert(order);
// 3. 扣减用户余额
userDao.deductBalance(userId, order.getAmount());
// 以上三步作为一个原子事务
}
}
设计思路:
- 并发编程原子性:内存预扣减,快速拦截无效请求
- 数据库原子性:持久化订单数据,保证业务完整性
- 补偿机制:失败时回补内存库存
深层次的区别
1. 抽象层次不同
并发编程原子性:
应用层
↓
JVM层
↓
操作系统层
↓
CPU指令层 ← 原子性在这里保证
数据库原子性:
应用层
↓
数据库引擎层 ← 原子性在这里保证
↓
存储引擎层
↓
文件系统层
2. 故障范围不同
// 并发编程:只考虑线程调度
synchronized (lock) {
count++;
}
// 如果JVM崩溃,数据丢失
// 数据库:考虑系统故障
@Transactional
public void transfer() {
deduct();
add();
}
// 即使数据库崩溃重启,也能恢复或回滚
3. 实现机制不同
| 实现机制 | 并发编程 | 数据库 |
|---|---|---|
| 底层基础 | CPU原子指令(CAS、LOCK) | 日志系统(Undo/Redo Log) |
| 锁机制 | Monitor、ReentrantLock | 行锁、表锁、间隙锁 |
| 可见性 | volatile、内存屏障 | 事务隔离级别、MVCC |
| 持久化 | 不涉及 | WAL(Write-Ahead Logging) |
面试总结
核心要点
- 不完全一样:虽然都是”全或无”,但层次、范围、实现完全不同
- 并发编程原子性:关注线程调度、CPU指令、内存可见性
- 数据库原子性:关注事务完整性、持久化、故障恢复
- 实际应用:常常需要组合使用
答题模板
简明版:
- 都保证”全或无”,但层次不同
- 并发编程关注CPU指令级别的原子性(纳秒)
- 数据库关注事务级别的原子性(毫秒)
- 并发编程在内存,数据库涉及持久化
完整版:
- 相同点:
- 都是”全或无”的保证
- 都需要隔离机制
- 都有性能开销
- 不同点:
- 层次:并发编程在CPU指令级,数据库在事务级
- 时间:并发编程纳秒级,数据库毫秒到秒级
- 范围:并发编程单个/少量操作,数据库多个SQL
- 持久化:并发编程不涉及,数据库保证持久化
- 恢复:并发编程重试/异常,数据库回滚到事务前
- 实现机制:
- 并发编程:CAS、Lock、volatile、内存屏障
- 数据库:Undo Log、Redo Log、MVCC、锁
- 应用场景:
- 并发编程:内存计数器、缓存更新、状态标志
- 数据库:转账、订单创建、库存扣减+记录日志
- 组合使用:
- 内存快速筛选(并发编程原子性)
- 数据库保证持久化(数据库原子性)
典型面试对话
面试官:并发编程的原子性和数据库ACID的原子性一样吗?
候选人:不完全一样。虽然都保证”全或无”的语义,但它们在不同的层次解决不同的问题:
-
并发编程的原子性关注的是CPU指令级别,防止线程调度打断操作,时间粒度在纳秒级。比如
count++分为读、改、写三步,需要通过CAS或锁保证这三步不被其他线程打断。 -
数据库的原子性关注的是事务级别,保证多个SQL操作作为一个整体提交或回滚,时间粒度在毫秒到秒级。比如转账包含扣款和加款两步,要么都成功,要么都失败。
- 关键区别:
- 并发编程在内存中操作,不保证持久化
- 数据库操作会持久化,且支持故障恢复
- 并发编程通过CPU指令(如CAS)实现,数据库通过日志(Undo/Redo Log)实现
- 实际应用中,我们常常组合使用。比如秒杀场景,先用内存原子操作快速扣减库存(并发编程),再用数据库事务持久化订单(数据库原子性)。
常见追问
Q:为什么不能只用数据库原子性?
A:性能问题。数据库操作涉及磁盘I/O、网络通信,QPS只有几千;而内存操作可以达到百万级QPS。高并发场景必须结合使用。
Q:数据库的原子性能保证JVM崩溃后的恢复吗?
A:可以。数据库通过WAL(预写日志)机制,先写日志再写数据。即使数据库崩溃,重启后可以根据日志重做(Redo)已提交的事务,回滚(Undo)未提交的事务。
Q:并发编程中有类似数据库回滚的机制吗?
A:没有自动回滚机制。如果操作失败,需要程序员手动补偿。比如CAS失败会重试,但不会自动恢复之前的状态。这也是为什么高并发场景需要精心设计补偿逻辑。