深入理解乐观锁与悲观锁
在多线程编程中,锁是保证数据一致性的重要机制。本文将详细介绍两种常见的锁策略:乐观锁和悲观锁。
什么是锁?
在多线程环境下,多个线程同时访问共享资源时,可能会导致数据不一致的问题。比如两个线程同时读取同一个账户余额,然后都进行扣款操作,就可能出现重复扣款的情况。
java
// 银行账户类(线程不安全版本)
public class BankAccount {
private double balance; // 账户余额
// 构造函数,初始化余额
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
// 取款方法
public void withdraw(double amount) {
// 检查余额是否充足
if (balance >= amount) {
// 模拟业务处理时间(实际场景中可能是数据库操作)
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 扣减余额
balance -= amount;
System.out.println("取款成功,剩余余额: " + balance);
} else {
System.out.println("余额不足");
}
}
}上面的代码存在线程安全问题,即使余额足够,也可能出现超卖的情况。
悲观锁
概念
悲观锁认为:每次访问共享资源时,都会有其他线程来竞争。所以在访问之前,先把资源锁住,确保只有自己能用完再释放。
就像去图书馆借书,你怕别人也想要这本书,所以一去就先把书锁在柜子里,等你用完了再放回去。
实现方式
Java 中悲观锁主要有两种实现方式:
1. synchronized 关键字
java
// 使用 synchronized 实现的线程安全银行账户
public class SynchronizedBankAccount {
private double balance; // 账户余额
// 构造函数,初始化余额
public SynchronizedBankAccount(double initialBalance) {
this.balance = initialBalance;
}
// synchronized 保证方法同一时间只能被一个线程访问
public synchronized void withdraw(double amount) {
// 检查余额是否充足
if (balance >= amount) {
// 模拟业务处理时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 扣减余额
balance -= amount;
System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
}
}
}使用测试:
java
public class SynchronizedDemo {
public static void main(String[] args) {
// 创建余额为 1000 的账户
SynchronizedBankAccount account = new SynchronizedBankAccount(1000);
// 创建两个线程同时取款 500
Thread t1 = new Thread(() -> account.withdraw(500), "线程1");
Thread t2 = new Thread(() -> account.withdraw(500), "线程2");
// 启动线程
t1.start();
t2.start();
}
}执行结果:
线程1 取款成功,剩余余额: 500.0
线程2 余额不足2. ReentrantLock
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 使用 ReentrantLock 实现的线程安全银行账户
public class LockBankAccount {
private double balance; // 账户余额
private final Lock lock = new ReentrantLock(); // 创建可重入锁
// 构造函数,初始化余额
public LockBankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void withdraw(double amount) {
// 获取锁
lock.lock();
try {
// 检查余额是否充足
if (balance >= amount) {
// 模拟业务处理时间
Thread.sleep(100);
// 扣减余额
balance -= amount;
System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁(放在 finally 中确保一定执行)
lock.unlock();
}
}
}特点
- 优点:安全可靠,能保证数据一致性
- 缺点:性能开销大,锁竞争激烈时会降低系统性能
- 适用场景:写操作多、竞争激烈的场景
乐观锁
概念
乐观锁认为:每次访问共享资源时,不会有其他线程来竞争。所以不直接加锁,而是在更新数据时检查是否有其他线程修改过。
就像去超市买东西,你觉得货架上商品很充足,不用担心被人抢完,所以直接拿商品去结账。结账时再检查价格和库存,如果有问题再处理。
实现方式
乐观锁主要有两种实现方式:
1. 版本号机制(Version)
给每条数据加一个版本号,更新时检查版本号是否一致。
java
import java.util.concurrent.atomic.AtomicInteger;
// 使用版本号机制实现的乐观锁账户
public class OptimisticAccount {
private double balance; // 账户余额
private final AtomicInteger version = new AtomicInteger(0); // 版本号,使用原子类保证线程安全
// 构造函数,初始化余额
public OptimisticAccount(double initialBalance) {
this.balance = initialBalance;
}
public boolean withdraw(double amount) {
// 读取当前版本号
int currentVersion = version.get();
// 模拟业务处理时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 检查余额是否充足
if (balance >= amount) {
// 扣减余额
balance -= amount;
// 使用 CAS 更新版本号和余额
// compareAndSet 方法原子性地比较并更新值
if (version.compareAndSet(currentVersion, currentVersion + 1)) {
System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
return true;
} else {
System.out.println(Thread.currentThread().getName() + " 版本冲突,重试");
return false;
}
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
return false;
}
}
public double getBalance() {
return balance;
}
}更完整的版本号实现:
java
// 使用 synchronized 配合版本号的实现
public class VersionedAccount {
private double balance; // 账户余额
private long version; // 版本号
// 构造函数,初始化余额和版本号
public VersionedAccount(double initialBalance) {
this.balance = initialBalance;
this.version = 0;
}
public boolean withdraw(double amount) {
// 使用 synchronized 保证检查-更新操作的原子性
synchronized (this) {
// 检查余额是否充足
if (balance >= amount) {
// 模拟业务处理时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 扣减余额并更新版本号
balance -= amount;
version++;
System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
return true;
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足");
return false;
}
}
}
}2. CAS(Compare-And-Swap)
CAS 是 CPU 级别的原子操作,Java 中的 AtomicInteger、AtomicLong 等都是基于 CAS 实现的。
java
import java.util.concurrent.atomic.AtomicInteger;
// 使用 CAS 实现的银行账户(乐观锁)
public class CASBankAccount {
private final AtomicInteger balance; // 使用原子整数保证余额的原子操作
// 构造函数,初始化余额
public CASBankAccount(int initialBalance) {
this.balance = new AtomicInteger(initialBalance);
}
public boolean withdraw(int amount) {
// 循环直到成功或余额不足
while (true) {
// 读取当前余额
int currentBalance = balance.get();
// 检查余额是否充足
if (currentBalance < amount) {
System.out.println(Thread.currentThread().getName() + " 余额不足");
return false;
}
// 模拟业务处理时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// CAS 更新:如果当前余额等于读取的余额,则更新为新值
// compareAndSet 返回 true 表示成功,false 表示失败(被其他线程修改)
if (balance.compareAndSet(currentBalance, currentBalance - amount)) {
System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + (currentBalance - amount));
return true;
}
// CAS 失败,说明有其他线程修改了余额,进入下一轮循环重试
}
}
}使用测试:
java
public class CASDemo {
public static void main(String[] args) {
// 创建余额为 1000 的账户
CASBankAccount account = new CASBankAccount(1000);
// 创建三个线程同时取款 500
Thread t1 = new Thread(() -> account.withdraw(500), "线程1");
Thread t2 = new Thread(() -> account.withdraw(500), "线程2");
Thread t3 = new Thread(() -> account.withdraw(500), "线程3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}执行结果:
线程1 取款成功,剩余余额: 500
线程2 余额不足
线程3 余额不足特点
- 优点:性能高,不阻塞线程,适合读多写少的场景
- 缺点:ABA 问题,需要处理重试逻辑
- 适用场景:读操作多、写操作少、竞争不激烈的场景
乐观锁与悲观锁对比
| 特性 | 悲观锁 | 乐观锁 |
|---|---|---|
| 实现方式 | synchronized、ReentrantLock | CAS、版本号 |
| 线程模型 | 阻塞式 | 非阻塞式 |
| 适用场景 | 写多、竞争激烈 | 读多、写少 |
| 性能开销 | 高 | 低 |
| 复杂度 | 低 | 高 |
| 数据安全 | 高 | 较高 |
实际应用场景
悲观锁适用场景
java
// 电商库存服务(悲观锁实现)
public class InventoryService {
private int stock; // 库存数量
private final Lock lock = new ReentrantLock(); // 库存锁
/**
* 扣减库存
* @param quantity 要扣减的数量
* @return true-扣减成功,false-库存不足
*/
public boolean reduceStock(int quantity) {
// 获取锁
lock.lock();
try {
// 检查库存是否充足
if (stock >= quantity) {
// 扣减库存
stock -= quantity;
System.out.println("扣减库存成功,剩余库存: " + stock);
return true;
}
System.out.println("库存不足");
return false;
} finally {
// 释放锁
lock.unlock();
}
}
}乐观锁适用场景
java
// 用户服务(乐观锁实现)
public class UserService {
private int version; // 版本号,用于乐观锁控制
/**
* 更新用户信息
* @param user 要更新的用户对象
* @return true-更新成功,false-版本冲突,需要重试
*/
public boolean updateUser(User user) {
// 读取当前版本号(在业务处理之前)
int currentVersion = version;
// 业务处理...(例如验证、更新数据等)
user.setName("新名称");
user.setUpdateTime(System.currentTimeMillis());
// 更新时检查版本号是否发生变化
if (version == currentVersion) {
// 版本一致,更新数据并递增版本号
version++;
System.out.println("用户更新成功");
return true;
}
// 版本已过期,说明数据被其他线程修改,需要重试
System.out.println("版本冲突,更新失败");
return false;
}
}总结
悲观锁:保守策略,适合竞争激烈、写操作多的场景,能保证数据安全但性能较低
乐观锁:激进策略,适合读多写少、竞争不激烈的场景,性能高但需要处理重试
选择建议:
- 写操作多、竞争激烈 -> 悲观锁
- 读操作多、写操作少 -> 乐观锁
- 不确定 -> 先用乐观锁,必要时再升级为悲观锁
持续更新中...