核心概念

并发编程的原子性

并发编程中的原子性 是指一个或多个操作在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. 并发编程原子性:内存预扣减,快速拦截无效请求
  2. 数据库原子性:持久化订单数据,保证业务完整性
  3. 补偿机制:失败时回补内存库存

深层次的区别

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)

面试总结

核心要点

  1. 不完全一样:虽然都是”全或无”,但层次、范围、实现完全不同
  2. 并发编程原子性:关注线程调度、CPU指令、内存可见性
  3. 数据库原子性:关注事务完整性、持久化、故障恢复
  4. 实际应用:常常需要组合使用

答题模板

简明版

  • 都保证”全或无”,但层次不同
  • 并发编程关注CPU指令级别的原子性(纳秒)
  • 数据库关注事务级别的原子性(毫秒)
  • 并发编程在内存,数据库涉及持久化

完整版

  1. 相同点
    • 都是”全或无”的保证
    • 都需要隔离机制
    • 都有性能开销
  2. 不同点
    • 层次:并发编程在CPU指令级,数据库在事务级
    • 时间:并发编程纳秒级,数据库毫秒到秒级
    • 范围:并发编程单个/少量操作,数据库多个SQL
    • 持久化:并发编程不涉及,数据库保证持久化
    • 恢复:并发编程重试/异常,数据库回滚到事务前
  3. 实现机制
    • 并发编程:CAS、Lock、volatile、内存屏障
    • 数据库:Undo Log、Redo Log、MVCC、锁
  4. 应用场景
    • 并发编程:内存计数器、缓存更新、状态标志
    • 数据库:转账、订单创建、库存扣减+记录日志
  5. 组合使用
    • 内存快速筛选(并发编程原子性)
    • 数据库保证持久化(数据库原子性)

典型面试对话

面试官:并发编程的原子性和数据库ACID的原子性一样吗?

候选人:不完全一样。虽然都保证”全或无”的语义,但它们在不同的层次解决不同的问题:

  1. 并发编程的原子性关注的是CPU指令级别,防止线程调度打断操作,时间粒度在纳秒级。比如count++分为读、改、写三步,需要通过CAS或锁保证这三步不被其他线程打断。

  2. 数据库的原子性关注的是事务级别,保证多个SQL操作作为一个整体提交或回滚,时间粒度在毫秒到秒级。比如转账包含扣款和加款两步,要么都成功,要么都失败。

  3. 关键区别
    • 并发编程在内存中操作,不保证持久化
    • 数据库操作会持久化,且支持故障恢复
    • 并发编程通过CPU指令(如CAS)实现,数据库通过日志(Undo/Redo Log)实现
  4. 实际应用中,我们常常组合使用。比如秒杀场景,先用内存原子操作快速扣减库存(并发编程),再用数据库事务持久化订单(数据库原子性)。

常见追问

Q:为什么不能只用数据库原子性?

A:性能问题。数据库操作涉及磁盘I/O、网络通信,QPS只有几千;而内存操作可以达到百万级QPS。高并发场景必须结合使用。

Q:数据库的原子性能保证JVM崩溃后的恢复吗?

A:可以。数据库通过WAL(预写日志)机制,先写日志再写数据。即使数据库崩溃,重启后可以根据日志重做(Redo)已提交的事务,回滚(Undo)未提交的事务。

Q:并发编程中有类似数据库回滚的机制吗?

A:没有自动回滚机制。如果操作失败,需要程序员手动补偿。比如CAS失败会重试,但不会自动恢复之前的状态。这也是为什么高并发场景需要精心设计补偿逻辑。