在并发编程和数据库操作场景中,多个线程或事务同时操作同一份数据时,很容易出现数据不一致的问题,悲观锁和乐观锁就是用来解决这类并发冲突的两种核心思路,二者的设计理念和适用场景有非常明显的区别。

悲观锁的核心概念与实现
悲观锁的核心假设是:在数据处理过程中,大概率会出现其他事务或线程来竞争修改同一份数据,因此会在操作数据前就先加锁,确保自己操作期间其他竞争者无法修改数据。
数据库层面的悲观锁实现
在关系型数据库中,最常用的悲观锁实现是SELECT ... FOR UPDATE语句,该语句会对查询到的行加上排他锁,其他事务想要对这些行执行修改或者加锁操作都会被阻塞,直到当前事务提交释放锁。
下面是一个MySQL中使用悲观锁的示例:
-- 开启事务 START TRANSACTION; -- 查询id为1的用户余额并加悲观锁 SELECT balance FROM user_account WHERE id = 1 FOR UPDATE; -- 执行业务逻辑,比如扣除100元 UPDATE user_account SET balance = balance - 100 WHERE id = 1; -- 提交事务,释放锁 COMMIT;
Java中的悲观锁实现
在Java并发编程中,synchronized关键字和ReentrantLock都是典型的悲观锁实现,会在获取资源前先尝试获取锁,获取不到就会进入阻塞状态。
以下是synchronized的使用示例:
public class PessimisticLockDemo {
private static final Object lock = new Object();
private static int count = 0;
public static void increment() {
// 加悲观锁,同一时间只有一个线程能执行同步块内的逻辑
synchronized (lock) {
count++;
}
}
}
乐观锁的核心概念与实现
乐观锁的核心假设是:在数据处理过程中,大概率不会有其他事务或线程来竞争修改同一份数据,因此不会提前加锁,而是在更新数据时判断这段时间内有没有其他操作者修改过数据,如果有就放弃本次操作或者重试。
数据库层面的乐观锁实现
数据库中最常用的乐观锁实现是版本号机制,也就是给数据表增加一个版本号字段,每次更新数据时版本号加1,更新时同时校验版本号是否和查询时的一致。
下面是版本号机制的实现示例:
-- 查询数据时获取当前版本号 SELECT id, balance, version FROM user_account WHERE id = 1; -- 假设查询到的version是5,更新时校验版本号,同时版本号加1 UPDATE user_account SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 5; -- 如果受影响行数为0,说明版本号已经被修改,更新失败
Java中的乐观锁实现
Java中AtomicInteger等原子类就是基于乐观锁的CAS(Compare And Swap)机制实现的,会在更新值时比较当前值和预期值是否一致,一致才更新成功。
以下是AtomicInteger的使用示例:
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockDemo {
private static AtomicInteger count = new AtomicInteger(0);
public static void increment() {
// CAS操作,预期值是当前值,更新为新值
int current;
do {
current = count.get();
} while (!count.compareAndSet(current, current + 1));
}
}
悲观锁和乐观锁的核心区别
两者的核心差异主要体现在以下几个维度:
| 对比维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 加锁时机 | 操作数据前就加锁 | 更新数据时才校验冲突 |
| 阻塞情况 | 获取不到锁会阻塞等待 | 不会阻塞,冲突时更新失败 |
| 适用场景 | 写操作多、冲突概率高的场景 | 读操作多、冲突概率低的场景 |
| 实现复杂度 | 依赖数据库或语言内置锁机制,实现简单 | 需要额外维护版本号或CAS逻辑,实现稍复杂 |
| 性能表现 | 频繁加锁释放锁,高并发下性能较差 | 无锁竞争,高并发下读多写少场景性能更好 |
两者的适用场景
适合用悲观锁的场景
- 数据冲突概率非常高,比如库存扣减场景,同一个商品同时被大量用户下单,写操作非常频繁,用悲观锁可以避免大量的更新失败重试。
- 业务逻辑复杂,操作数据的时间较长,提前加锁可以避免长事务带来的数据不一致问题。
- 对数据一致性要求极高,不允许出现任何更新失败的情况,比如金融转账的核心账务处理。
适合用乐观锁的场景
- 读操作远多于写操作,比如用户个人信息修改,大部分时间都是查询信息,很少修改,冲突概率极低。
- 系统并发量很高,悲观锁的阻塞等待会带来大量的线程上下文切换开销,用乐观锁可以提升系统吞吐量。
- 可以接受更新失败时让用户重试,比如用户提交表单时提示数据已被修改,请重新加载后提交。
使用时的注意事项
使用悲观锁时要注意控制加锁的范围和时间,避免长事务持有锁导致其他请求阻塞,甚至出现死锁问题。使用乐观锁时要注意ABA问题,也就是数据被修改后又改回原值,CAS机制无法识别这种情况,必要时可以结合版本号或者增加额外的状态字段来避免。另外乐观锁更新失败时需要有合理的重试机制,避免无限重试消耗系统资源。