Java 中悲观锁的概念与实现方式
1. 悲观锁的概念
悲观锁(Pessimistic Locking)是一种并发控制机制,它假设在并发环境中,数据冲突是不可避免的,因此在操作共享资源时,总是先加锁,确保其他线程在锁释放前无法访问或修改该资源。悲观锁适合在高并发且数据冲突频繁的场景中使用。
2. 悲观锁的实现方式
2.1 使用 synchronized
关键字
synchronized
是 Java 中最常用的悲观锁实现方式,用于对代码块或方法进行同步,确保同一时间只有一个线程可以执行被锁定的代码。
实现步骤:
-
同步代码块:
java复制
public class PessimisticLockExample { private Object lock = new Object(); public void updateData() { synchronized (lock) { // 被锁定的代码块 System.out.println("Updating data..."); } } }
-
同步方法:
java复制
public class PessimisticLockExample { public synchronized void updateData() { // 整个方法被锁定 System.out.println("Updating data..."); } }
优点:
- 简单易用:Java 内置支持,代码简洁。
- 自动释放锁:异常或方法结束时会自动释放锁。
缺点:
- 性能问题:锁的粒度较大时,可能导致性能瓶颈。
- 死锁风险:不当使用可能导致死锁。
2.2 使用 ReentrantLock
类
ReentrantLock
是 Java 提供的显式锁,相较于 synchronized
,它提供了更多的功能,如尝试锁、非阻塞锁、公平锁等。
实现步骤:
-
创建锁对象:
java复制
import java.util.concurrent.locks.ReentrantLock; public class PessimisticLockExample { private ReentrantLock lock = new ReentrantLock(); public void updateData() { lock.lock(); // 获取锁 try { // 被锁定的代码块 System.out.println("Updating data..."); } finally { lock.unlock(); // 确保释放锁 } } }
-
尝试获取锁:
java复制
if (lock.tryLock()) { try { // 被锁定的代码块 System.out.println("Updating data..."); } finally { lock.unlock(); } } else { System.out.println("Failed to acquire lock"); }
优点:
- 灵活性高:支持可中断锁、尝试锁、公平锁等。
- 功能丰富:可以更精细地控制锁的行为。
缺点:
- 手动释放锁:需要显式调用
unlock()
,否则可能导致死锁。 - 代码复杂性:相较于
synchronized
,实现稍显复杂。
2.3 使用数据库的悲观锁机制
在数据库层面,可以通过 SELECT ... FOR UPDATE
或 SELECT ... FOR SHARE
语句实现悲观锁,确保数据在事务提交前不会被其他事务修改。
实现步骤:
-
使用
SELECT ... FOR UPDATE
:sql复制
-- 查询数据并加锁 SELECT * FROM tb_voucher_order WHERE voucher_id = 1 FOR UPDATE;
-
更新数据:
sql复制
-- 更新数据 UPDATE tb_voucher_order SET stock = stock - 1 WHERE voucher_id = 1;
-
Java 实现:
java复制
@Transactional public void updateVoucherOrder(Long voucherId) { // 使用 FOR UPDATE 查询 SeckillVoucher voucher = seckillVoucherService.getForUpdateById(voucherId); // 扣减库存 voucher.setStock(voucher.getStock() - 1); seckillVoucherService.save(voucher); // 创建订单 VoucherOrder voucherOrder = new VoucherOrder(); voucherOrder.setUserId(UserHolder.getUser().getId()); voucherOrder.setVoucherId(voucherId); voucherOrder.save(); }
优点:
- 强一致性:确保数据在事务提交前不会被其他事务修改。
- 简单易用:直接利用数据库的锁机制。
缺点:
- 性能问题:在高并发场景下,可能导致数据库锁等待时间过长。
- 死锁风险:不当使用可能导致死锁。
2.4 使用 Lock
接口的其他实现类
除了 ReentrantLock
,Java 还提供了其他锁实现类,如 ReadWriteLock
和 StampedLock
。
2.4.1 使用 ReadWriteLock
ReadWriteLock
支持读写分离,允许多个读操作同时进行,但写操作时会独占锁。
实现步骤:
-
创建锁对象:
java复制
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class PessimisticLockExample { private ReadWriteLock lock = new ReentrantReadWriteLock(); public void updateData() { lock.writeLock().lock(); // 获取写锁 try { System.out.println("Updating data..."); } finally { lock.writeLock().unlock(); } } public void readData() { lock.readLock().lock(); // 获取读锁 try { System.out.println("Reading data..."); } finally { lock.readLock().unlock(); } } }
优点:
- 读写分离:允许多个读操作同时进行,提高并发性能。
- 适合读多写少的场景:如查询多、更新少的业务。
缺点:
- 复杂性:实现和使用相对复杂。
2.4.2 使用 StampedLock
StampedLock
是 Java 8 引入的一种乐观锁和悲观锁结合的锁实现,支持读、写和乐观读模式。
实现步骤:
-
创建锁对象:
java复制
import java.util.concurrent.locks.StampedLock; public class PessimisticLockExample { private StampedLock lock = new StampedLock(); public void updateData() { long stamp = lock.writeLock(); // 获取写锁 try { System.out.println("Updating data..."); } finally { lock.unlockWrite(stamp); } } public void readData() { long stamp = lock.readLock(); // 获取读锁 try { System.out.println("Reading data..."); } finally { lock.unlockRead(stamp); } } }
优点:
- 高性能:结合了悲观锁和乐观锁的优点。
- 灵活性:支持多种锁模式。
缺点:
- 复杂性:实现和使用相对复杂。
2.5 使用分布式锁
在分布式系统中,可以使用 ZooKeeper 或 Redis 实现悲观锁。
2.5.1 使用 Redis 实现分布式锁
java复制
public class DistributedLockExample {
private RedisTemplate<String, String> redisTemplate;
public boolean acquireLock(String lockKey) {
// 尝试获取锁
String lockValue = UUID.randomUUID().toString();
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
return success != null && success;
}
public void releaseLock(String lockKey, String lockValue) {
// 获取当前锁的值
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (currentValue != null && currentValue.equals(lockValue)) {
// 释放锁
redisTemplate.delete(lockKey);
}
}
}
2.5.2 使用 ZooKeeper 实现分布式锁
java复制
public class DistributedLockExample {
private CuratorFramework client;
public void acquireLock(String lockPath) throws Exception {
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
lock.acquire();
}
public void releaseLock(String lockPath) throws Exception {
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
lock.release();
}
}
优点:
- 分布式支持:适用于分布式系统。
- 高一致性:确保分布式环境下的数据一致性。
缺点:
- 依赖外部服务:需要依赖 ZooKeeper 或 Redis 等外部服务。
- 复杂性:实现和维护成本较高。
总结
悲观锁的实现方式多样,可以根据具体需求选择合适的方式:
synchronized
:简单易用,适合单线程环境。ReentrantLock
:功能丰富,适合需要更多控制的场景。- 数据库锁:直接利用数据库的锁机制,适合数据一致性要求高的场景。
- 分布式锁:适用于分布式系统,确保数据一致性。