🔐 乐观锁 (Optimistic Lock) 与 悲观锁 (Pessimistic Lock)
在数据库、分布式系统及多线程编程中,乐观锁与悲观锁是两种重要的并发控制机制。它们在数据一致性、性能及使用场景方面各有优势。
📌 一、核心概念
🔎 1. 悲观锁 (Pessimistic Lock)
“悲观”地认为数据会发生冲突,因此主动加锁以防止其他线程修改数据。
- 每次访问数据时,都会先加锁,其他线程只能等待锁释放。
- 适用于写操作频繁、冲突较多的场景。
- 常用于:数据库行锁、文件锁、Synchronized (Java) 等。
✅ 优点:数据一致性强,适用于竞争严重的场景。
❗️缺点:锁开销较大,容易导致性能瓶颈。
🔎 2. 乐观锁 (Optimistic Lock)
“乐观”地认为数据冲突较少,因此不主动加锁,而是在更新数据时检测冲突。
- 通常使用版本号 (Version) 或 时间戳 (Timestamp) 来判断数据是否被其他线程修改。
- 适用于读操作频繁、冲突较少的场景。
- 常用于:CAS (Compare-And-Swap)、数据库的
UPDATE WHERE
条件等。
✅ 优点:无锁操作,性能高,适用于竞争较少的场景。
❗️缺点:数据冲突时,重试成本较高。
📋 二、两者对比
特点 | 悲观锁 (Pessimistic Lock) | 乐观锁 (Optimistic Lock) |
---|---|---|
加锁机制 | 访问数据时主动加锁; | 访问数据时不加锁,更新时检查冲突; |
性能 | 并发量较低,线程/事务需等待锁释放; | 并发量较高,避免不必要的锁开销; |
数据一致性 | 数据一致性更强; | 可能存在更新失败的情况; |
适用场景 | 写多读少,数据冲突频繁; | 读多写少,数据冲突较少; |
实现方式 | 锁机制 (Synchronized 、Lock 等); | 版本号 (Version)、时间戳 (Timestamp)、CAS; |
🚀 三、示例代码
🔥 1. 悲观锁示例 (MySQL 行锁)
在 MySQL 中,
SELECT ... FOR UPDATE
会锁住查询到的行,防止其他事务修改。
-- 事务 1:获取悲观锁并更新数据
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 事务 2:等待锁释放才能执行
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 阻塞,等待事务 1 完成
UPDATE accounts SET balance = balance + 200 WHERE id = 1;
COMMIT;
✅ 优势:确保在整个事务中数据一致性强;
❗️劣势:锁的开销较大,易导致性能瓶颈。
🔥 2. 乐观锁示例 (MySQL 版本号)
通过版本号来判断数据是否被其他线程修改,避免加锁。
表结构
CREATE TABLE accounts (
id INT PRIMARY KEY,
balance DECIMAL(10, 2),
version INT -- 版本号字段
);
更新数据
-- 第一次读取数据 (版本号为 1)
SELECT balance, version FROM accounts WHERE id = 1;
-- 更新数据时检查版本号是否变化
UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 1;
-- 如果受影响行数为 0,表示版本号不匹配,更新失败,需重试
✅ 优势:无锁操作,性能高;
❗️劣势:数据冲突时,重试成本较高。
🔥 3. Java 中的悲观锁示例 (Synchronized)
public class PessimisticLockExample {
private int count = 0;
// 使用 synchronized 作为悲观锁
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
public static void main(String[] args) {
PessimisticLockExample example = new PessimisticLockExample();
Thread t1 = new Thread(example::increment);
Thread t2 = new Thread(example::increment);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终结果:" + example.getCount());
}
}
✅ 输出示例
最终结果:2
🔥 4. Java 中的乐观锁示例 (CAS - Compare-And-Swap)
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
int current;
do {
current = count.get();
} while (!count.compareAndSet(current, current + 1));
}
public int getCount() {
return count.get();
}
public static void main(String[] args) {
OptimisticLockExample example = new OptimisticLockExample();
Thread t1 = new Thread(example::increment);
Thread t2 = new Thread(example::increment);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终结果:" + example.getCount());
}
}
✅ 输出示例
最终结果:2
🔎 CAS 操作会不断尝试更新值,直至成功,避免了锁的阻塞。
📌 四、应用场景总结
场景 | 推荐使用 |
---|---|
频繁写操作,竞争激烈 | ✅ 悲观锁 |
读多写少,数据冲突少 | ✅ 乐观锁 |
数据一致性要求极高 (如金融系统) | ✅ 悲观锁 |
高性能要求,减少锁开销 | ✅ 乐观锁 |
高并发场景下的短时间冲突 | ✅ 乐观锁 |
🌟 总结
- 悲观锁更注重数据的一致性,适用于写多读少、冲突频繁的场景;
- 乐观锁更注重性能,适用于读多写少、冲突较少的场景;
- 乐观锁不使用锁机制,避免了线程阻塞,性能更优,但需处理冲突重试问题;
- 悲观锁在竞争严重时更稳定,适用于关键业务。
💡 在选择锁机制时,需根据具体业务场景、数据访问模式及性能需求来选择最佳策略。