悲观锁和乐观锁是两种常见的并发控制策略,用于解决多线程或多进程环境下共享资源访问的冲突问题。它们基于不同的假设和实现方式,适用于不同的业务场景。
悲观锁(Pessimistic Locking)
基本概念
悲观锁是一种"悲观"的并发控制机制,它假设在多线程环境下,数据冲突是常态而非例外。因此在访问数据前,悲观锁会先获取锁,确保在持有锁的期间内,其他线程无法修改数据,从而避免并发冲突。
悲观锁的核心思想可以概括为:"宁可错杀一千,不可放过一个"。它在数据处理前就假设最坏的情况会发生,因此采取预防性的加锁措施。
主要特点
- 排他性:一旦某个线程获得锁,其他线程必须等待
- 阻塞性:获取锁失败的线程会被阻塞,直到锁被释放
- 一致性保证强:能有效避免脏读、不可重复读等问题
- 开销较大:加锁、释放锁需要额外资源
- 可能引发死锁:不正确的使用可能导致死锁情况
实现方式
在Java中,悲观锁主要通过以下方式实现:
-
synchronized关键字:Java内置的同步机制
typescript// 同步方法 public synchronized void increment() { count++; } // 同步代码块 synchronized(lock) { count++; }
-
ReentrantLock类:Java并发包中的显式锁
csharpprivate final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } }
-
数据库悲观锁 :如
SELECT ... FOR UPDATE
语句sqlSELECT * FROM accounts WHERE id = 1 FOR UPDATE;
适用场景
悲观锁最适合以下场景:
- 临界区执行时间长:当操作需要较长时间完成时
- 冲突频率高:当并发修改的概率很高时
- 强一致性要求:需要严格保证数据一致性的场景
- 写操作多:写多读少的场景
典型应用场景包括:
- 银行转账
- 库存扣减
- 订单处理
- 票务系统
乐观锁(Optimistic Locking)
基本概念
乐观锁是一种并发控制机制,它假设在大多数情况下,数据冲突的概率很低,因此在读取数据时不加锁,只在更新数据时检查是否有其他事务修改了数据。如果数据在读取后被其他事务修改,则更新失败,需要重新读取数据并重试更新操作。
乐观锁的核心思想是通过版本号或时间戳来记录数据的变更历史。每次更新数据时,都会检查版本号或时间戳是否与读取时的值一致。如果不一致,说明数据已经被其他事务修改,当前事务需要回滚并重试。
主要特点
- 无锁读取:读取数据时不加锁,提高并发性能
- 冲突检测:只在更新时检查数据是否被修改
- 重试机制:冲突时需要重试操作
- 适合读多写少:在低冲突场景下性能优越
实现方式
乐观锁可以通过以下几种方式实现:
-
版本号机制:在数据表中增加一个版本号字段,每次更新数据时,版本号加1
iniUPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 2;
-
时间戳机制:使用时间戳代替版本号,原理相同
sqlUPDATE users SET name = 'newName', timestamp = CURRENT_TIMESTAMP WHERE id = 1 AND timestamp = '2023-01-01 12:00:00';
-
CAS(Compare And Swap)算法:Java中的原子类实现
scssAtomicInteger version = new AtomicInteger(0); int current = version.get(); if(version.compareAndSet(current, current + 1)) { // 更新成功 } else { // 更新失败,重试 }
适用场景
乐观锁适用于读多写少的场景,特别是当冲突发生概率较低时效果最佳。例如:
- 电商系统中的库存管理:用户浏览商品时读取库存,下单时检查库存是否有变化
- 社交网络中的消息系统:用户读取消息列表时不需要加锁,发送新消息时检查是否有其他用户修改了消息状态
悲观锁与乐观锁的对比
特性 | 悲观锁 | 乐观锁 |
---|---|---|
假设 | 冲突会发生 | 冲突很少发生 |
实现方式 | 加锁(synchronized, ReentrantLock) | 版本号/时间戳/CAS |
并发性 | 低 | 高 |
开销 | 大 | 小 |
适用场景 | 写多读少 | 读多写少 |
冲突处理 | 阻塞等待 | 回滚重试 |
示例 | SELECT ... FOR UPDATE | AtomicInteger, 版本控制 |
优点 | 强一致性保证 | 高并发性能 |
缺点 | 性能开销大,可能死锁 | 高冲突时频繁重试 |
选择建议
-
悲观锁适用场景:
- 数据冲突概率高
- 对数据一致性要求严格
- 写操作远多于读操作
- 操作执行时间长
-
乐观锁适用场景:
- 数据冲突概率低
- 读操作远多于写操作
- 系统需要高并发性能
- 可以接受偶尔的重试
-
混合使用:
在实际应用中,可以根据不同的业务场景混合使用悲观锁和乐观锁。例如:
- 在订单系统中,使用悲观锁保证库存扣减的一致性
- 在新闻阅读系统中,使用乐观锁提高并发性能
实际应用示例
悲观锁示例:银行转账
csharp
public class BankAccount {
private double balance;
private final Lock lock = new ReentrantLock();
public void transfer(double amount) {
lock.lock();
try {
double newBalance = balance + amount;
Thread.sleep(100); // 模拟业务处理耗时
balance = newBalance;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
乐观锁示例:库存扣减
java
public class Product {
private int stock;
private final AtomicInteger version = new AtomicInteger(0);
public boolean deductStock() {
while (true) {
int currentVersion = version.get();
int currentStock = stock;
if (currentStock <= 0) return false;
if (version.compareAndSet(currentVersion, currentVersion + 1)) {
stock = currentStock - 1;
return true;
}
}
}
}
总结
悲观锁和乐观锁是两种互补的并发控制策略,各有优缺点和适用场景。理解它们的核心思想、实现方式和适用场景,可以帮助开发者在实际项目中做出合理的选择,构建高性能且数据一致的系统。