乐观锁与悲观锁-学习笔记
乐观锁与悲观锁 学习笔记
在多线程和分布式系统中,锁机制是保证数据一致性和并发控制的关键技术。乐观锁和悲观锁是两种常见的锁策略,它们在不同的场景下有着各自的优势和适用性。本文将详细介绍乐观锁和悲观锁的原理、区别以及代码示例,帮助你更好地理解和选择合适的锁机制。
前言
**悲观锁:假设冲突是常态,因此在操作数据时直接加锁,直到操作完成才释放锁。
乐观锁:假设冲突是少数情况,因此在操作数据时不加锁,但在更新数据时检查数据是否被其他操作修改。**
一、悲观锁
1.1 悲观锁的定义
悲观锁(Pessimistic Locking)是一种基于“悲观”假设的锁机制。它认为在并发环境中,数据冲突是常态,因此在操作数据时会先加锁,直到操作完成才会释放锁。这种方式类似于传统的关系型数据库中的锁机制,通过锁来防止其他线程或事务对数据的并发访问。
1.2 悲观锁的实现方式
悲观锁可以通过数据库的锁机制(如行锁、表锁)或编程语言中的锁(如 Java 中的 synchronized 或 ReentrantLock)来实现。
示例 1:使用 Java 的 synchronized 实现悲观锁
public class PessimisticLockExample {
private int count = 0;
// 使用 synchronized 方法锁
public synchronized void increment() {
int temp = count;
temp += 1;
count = temp;
}
public synchronized int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
PessimisticLockExample example = new PessimisticLockExample();
// 创建多个线程模拟并发
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.increment();
}
}).start();
}
Thread.sleep(2000); // 等待线程执行完成
System.out.println("Final count: " + example.getCount());
}
}
示例 2:使用数据库的悲观锁
在 SQL 中,可以通过
SELECT ... FOR UPDATE
语句实现悲观锁,锁定目标行,防止其他事务修改。
-- 假设有一个表 `account`
BEGIN TRANSACTION;
-- 锁定目标行
SELECT balance FROM account WHERE id = 1 FOR UPDATE;
-- 更新余额
UPDATE account SET balance = balance + 100 WHERE id = 1;
COMMIT;
1.3 悲观锁的优缺点
- 优点
:
- 能够有效防止数据冲突,保证数据一致性。
- 适合写操作频繁的场景。
- 缺点
:
- 锁的开销较大,可能导致性能瓶颈。
- 容易出现死锁问题。
二、乐观锁
2.1 乐观锁的定义
乐观锁(Optimistic Locking)基于“乐观”假设,认为在大多数情况下,数据冲突是很少发生的。因此,它不会在操作数据时直接加锁,而是通过版本号(Version Number)或时间戳(Timestamp)来检测数据是否被其他线程修改。
2.2 乐观锁的实现方式
乐观锁通常通过以下两种方式实现:
- 基于版本号(Version Number) :每次更新数据时,版本号加一,更新时检查版本号是否一致。
- 基于时间戳(Timestamp) :记录数据的最后修改时间戳,更新时检查时间戳是否被修改。
示例 1:基于版本号的乐观锁
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockExample {
private AtomicInteger version = new AtomicInteger(0);
private int count = 0;
public void increment() {
int currentVersion = version.get();
while (true) {
int temp = count;
temp += 1;
// 检查版本号是否被其他线程修改
if (version.compareAndSet(currentVersion, currentVersion + 1)) {
count = temp;
break;
} else {
// 如果版本号被修改,重新获取版本号并重试
currentVersion = version.get();
}
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
OptimisticLockExample example = new OptimisticLockExample();
// 创建多个线程模拟并发
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.increment();
}
}).start();
}
Thread.sleep(2000); // 等待线程执行完成
System.out.println("Final count: " + example.getCount());
}
}
示例 2:基于时间戳的乐观锁
在数据库中,可以通过记录时间戳来实现乐观锁。
-- 假设有一个表 `account`,包含 `balance` 和 `last_modified` 时间戳字段
BEGIN TRANSACTION;
-- 获取当前行的版本信息
SELECT balance, last_modified FROM account WHERE id = 1;
-- 更新余额并检查时间戳
UPDATE account
SET balance = balance + 100, last_modified = NOW()
WHERE id = 1 AND last_modified = '2025-03-14 10:00:00';
COMMIT;
2.3 乐观锁的优缺点
- 优点
:
- 减少了锁的开销,提高了系统性能。
- 适合读多写少的场景。
- 缺点
:
- 在高并发写操作场景下,可能会导致大量重试,性能下降。
- 实现相对复杂,需要额外的版本号或时间戳字段。
三、乐观锁与悲观锁的选择
选择乐观锁还是悲观锁,需要根据具体的业务场景来决定:
- 读多写少的场景 :优先选择乐观锁,因为它减少了锁的开销,提高了性能。
- 写操作频繁的场景 :优先选择悲观锁,因为它能够有效防止数据冲突。
- 对性能要求极高的场景 :可以尝试使用乐观锁,但需要仔细评估重试机制对性能的影响。
四、总结
乐观锁和悲观锁是并发控制中的两种重要策略。悲观锁通过加锁来防止数据冲突,适合写操作频繁的场景;乐观锁通过版本号或时间戳来检测数据冲突,适合读多写少的场景。在实际开发中,我们需要根据业务需求和性能要求,合理选择锁机制,以实现高效且可靠的并发控制。
希望本文对大家理解乐观锁和悲观锁有所帮助。如果你有任何疑问或建议,欢迎在评论区留言交流。