一、什么是悲观锁?
悲观锁(Pessimistic Locking)是一种并发控制机制,它基于一种"悲观"的假设:认为在多线程或多进程环境下,数据冲突是常态而非例外。因此,在访问数据前,悲观锁会先获取锁,确保在持有锁的期间内,其他线程无法修改数据,从而避免并发冲突。
悲观锁的核心思想可以概括为:"宁可错杀一千,不可放过一个"。它在数据处理前就假设最坏的情况会发生,因此采取预防性的加锁措施。
二、悲观锁的特点
- 排他性:一旦某个线程获得锁,其他线程必须等待
- 阻塞性:获取锁失败的线程会被阻塞,直到锁被释放
- 一致性保证强:能有效避免脏读、不可重复读等问题
- 开销较大:加锁、释放锁需要额外资源
- 可能引发死锁:不正确的使用可能导致死锁情况
三、悲观锁的实现方式
在Java中,悲观锁主要通过以下方式实现:
- synchronized关键字:Java内置的同步机制
- ReentrantLock类:Java并发包中的显式锁
- 数据库悲观锁:如SELECT ... FOR UPDATE
下面我们分别用代码示例来说明这几种实现方式。
四、synchronized实现悲观锁
synchronized是Java中最基本的悲观锁实现方式,它可以修饰方法或代码块。
1. 同步方法
public class SynchronizedExample {
private int count = 0;
// 同步方法
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
2. 同步代码块
public class SynchronizedBlockExample {
private int count = 0;
private final Object lock = new Object();
public void increment() {
// 同步代码块
synchronized(lock) {
count++;
}
}
public int getCount() {
synchronized(lock) {
return count;
}
}
}
3. 类级别锁
public class ClassLevelLock {
private static int count = 0;
public void increment() {
// 类级别锁
synchronized(ClassLevelLock.class) {
count++;
}
}
}
五、ReentrantLock实现悲观锁
ReentrantLock是Java并发包(java.util.concurrent.locks)中提供的显式锁实现,它比synchronized更灵活,提供了更多高级功能。
基本用法
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 确保锁被释放
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
尝试获取锁
public boolean tryIncrement() {
if (lock.tryLock()) { // 尝试获取锁,不阻塞
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false;
}
public boolean tryIncrementWithTimeout() throws InterruptedException {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试1秒内获取锁
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false;
}
公平锁与非公平锁
// 非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock();
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
公平锁会按照线程请求锁的顺序来获取锁,而非公平锁则允许"插队",可能提高吞吐量但可能导致某些线程饥饿。
六、数据库悲观锁
在数据库层面,悲观锁通常通过SELECT ... FOR UPDATE语句实现。
JDBC示例
import java.sql.*;
public class DatabasePessimisticLock {
public void transferMoney(Connection conn, int fromId, int toId, int amount) throws SQLException {
conn.setAutoCommit(false);
try {
// 锁定from账户
PreparedStatement stmt1 = conn.prepareStatement(
"SELECT balance FROM accounts WHERE id = ? FOR UPDATE");
stmt1.setInt(1, fromId);
ResultSet rs1 = stmt1.executeQuery();
if (!rs1.next() || rs1.getInt("balance") < amount) {
throw new RuntimeException("Insufficient balance");
}
// 锁定to账户
PreparedStatement stmt2 = conn.prepareStatement(
"SELECT balance FROM accounts WHERE id = ? FOR UPDATE");
stmt2.setInt(1, toId);
stmt2.executeQuery();
// 更新from账户
PreparedStatement stmt3 = conn.prepareStatement(
"UPDATE accounts SET balance = balance - ? WHERE id = ?");
stmt3.setInt(1, amount);
stmt3.setInt(2, fromId);
stmt3.executeUpdate();
// 更新to账户
PreparedStatement stmt4 = conn.prepareStatement(
"UPDATE accounts SET balance = balance + ? WHERE id = ?");
stmt4.setInt(1, amount);
stmt4.setInt(2, toId);
stmt4.executeUpdate();
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
}
}
JPA/Hibernate示例
@Entity
public class Account {
@Id
private Long id;
private int balance;
// getters and setters
}
public class AccountService {
@PersistenceContext
private EntityManager em;
@Transactional
public void transferMoney(Long fromId, Long toId, int amount) {
// 悲观锁查询
Account fromAccount = em.find(Account.class, fromId, LockModeType.PESSIMISTIC_WRITE);
Account toAccount = em.find(Account.class, toId, LockModeType.PESSIMISTIC_WRITE);
if (fromAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
fromAccount.setBalance(fromAccount.getBalance() - amount);
toAccount.setBalance(toAccount.getBalance() + amount);
}
}
七、悲观锁的适用场景
悲观锁最适合以下场景:
- 临界区执行时间长:当操作需要较长时间完成时
- 冲突频率高:当并发修改的概率很高时
- 强一致性要求:需要严格保证数据一致性的场景
- 写操作多:写多读少的场景
典型应用场景包括:
- 银行转账
- 库存扣减
- 订单处理
- 票务系统
八、悲观锁的优缺点
优点:
- 实现简单直观
- 保证强一致性
- 避免脏读、不可重复读等问题
- 适合冲突频繁的场景
缺点:
- 性能开销大(加锁、释放锁)
- 可能引起线程阻塞和等待
- 可能导致死锁
- 降低系统吞吐量
- 不适用于高并发读场景
九、悲观锁与乐观锁的对比
|
特性 |
悲观锁 |
乐观锁 |
|
假设 |
冲突会发生 |
冲突很少发生 |
|
实现方式 |
加锁 |
版本号/时间戳 |
|
并发性 |
低 |
高 |
|
开销 |
大 |
小 |
|
适用场景 |
写多读少 |
读多写少 |
|
冲突处理 |
阻塞等待 |
回滚重试 |
|
示例 |
synchronized, ReentrantLock |
CAS, 版本控制 |
十、悲观锁的最佳实践
- 尽量缩小锁范围:只锁定必要的代码段
- 减少锁持有时间:尽快释放锁
- 避免嵌套锁:防止死锁
- 使用try-finally确保释放锁:防止锁泄漏
- 考虑锁的粒度:根据场景选择合适粒度的锁
- 文档化锁策略:记录代码中的锁使用方式
// 最佳实践示例
public class BestPracticeExample {
private final ReentrantLock lock = new ReentrantLock();
private Map<String, String> cache = new HashMap<>();
public void updateCache(String key, String value) {
// 只锁定必要的代码段
lock.lock();
try {
cache.put(key, value);
} finally {
lock.unlock();
}
}
public String getValue(String key) {
// 读操作可以使用更轻量级的锁
lock.lock();
try {
return cache.get(key);
} finally {
lock.unlock();
}
}
}
十一、常见问题与解决方案
1. 死锁问题
问题描述:
// 线程1
synchronized(resourceA) {
synchronized(resourceB) {
// ...
}
}
// 线程2
synchronized(resourceB) {
synchronized(resourceA) {
// ...
}
}
解决方案:
- 按固定顺序获取锁
- 使用tryLock()设置超时
- 使用锁检测工具
2. 锁粒度过大
问题代码:
public synchronized void processOrder() {
// 整个方法被锁定,包括IO操作
readFromDB();
calculate();
writeToDB();
}
改进方案:
public void processOrder() {
// 只锁定必要部分
synchronized(this) {
readFromDB();
calculate();
}
writeToDB();
}
3. 锁泄漏
问题代码:
public void riskyMethod() throws Exception {
lock.lock();
someOperationThatMayThrowException();
lock.unlock(); // 如果抛出异常,锁不会被释放
}
解决方案:
public void safeMethod() throws Exception {
lock.lock();
try {
someOperationThatMayThrowException();
} finally {
lock.unlock();
}
}
十二、总结
悲观锁是一种重要的并发控制机制,它通过预先加锁的方式确保数据一致性。在Java中,我们可以通过synchronized关键字、ReentrantLock类或在数据库层面实现悲观锁。虽然悲观锁会带来一定的性能开销,但在高冲突、强一致性的场景下,仍然是不可或缺的工具。
合理使用悲观锁需要权衡性能与一致性,遵循最佳实践,避免常见陷阱。在系统设计时,应根据具体场景选择最合适的并发控制策略,有时悲观锁与乐观锁的结合使用可能会达到最佳效果。
233

被折叠的 条评论
为什么被折叠?



