目录
乐观锁和悲观锁是在并发编程中用于处理数据一致性和并发控制的两种不同的策略。
乐观锁(Optimistic Locking)
在Java中,乐观锁的思想是,假设并发的事务之间很少产生冲突,因此不需要阻塞其他事务的执行。在读取数据时,并不会加锁,而是在更新数据时才进行检查。乐观锁的实现通常使用CAS指令(Compare And Swap)操作。CAS是一种无锁算法,它通过比较并交换的方式,实现对共享变量的原子更新。CAS操作包括三个操作数:被操作的内存值V、预期的值A和新值B。当且仅当V的值等于A时,CAS才会将V的值更新为B,否则不进行任何操作。
乐观锁的实现思路如下:
- 获取数据的版本号version;
- 在更新数据时,先检查数据的版本号是否与之前读取的版本号相同;
- 如果相同,则执行更新操作,并将版本号加一;
- 如果不同,则说明有其他线程已经修改了数据,需要重新读取数据库中的数据,并重复上述步骤。
Java中常见的乐观锁实现方式有:
- Atomic类:例如AtomicInteger、AtomicLong等,可以通过原子性的compareAndSet()方法来实现乐观锁;
- 版本号控制:通过记录数据的版本号,判断数据是否被修改过。
注意:乐观锁的实现通常通过版本号(Versioning)或时间戳(Timestamp)来实现。当一个事务要更新一条数据时,先读取数据的当前版本号,然后执行更新操作。在提交事务时,检查数据库中的版本号是否与事务开始时读取的版本号相匹配,如果匹配则提交成功,否则则说明在提交之前有其他事务已经对数据进行了修改,此时需要回滚并重新尝试。
乐观锁的优点是并发性能较高,不需要阻塞其他事务的执行,但缺点是在并发冲突较多的情况下,需要频繁地回滚和重试,可能会影响性能。
悲观锁(Pessimistic Locking)
在Java中, 悲观锁的思想是,假设并发的事务之间经常产生冲突,因此在操作数据时会先将其锁定,以防止其他事务对数据进行修改。悲观锁通常使用synchronized关键字、ReentrantLock等锁机制来实现。这些锁机制都可以保证同一时间只有一个线程可以访问共享资源,其他线程需要等待锁的释放才能继续执行。
悲观锁的实现思路如下:
- 在读取数据时,对数据进行加锁;
- 在更新数据时,先对数据进行加锁,防止其他线程修改该数据;
- 在提交事务时,释放对数据的加锁。
Java中常见的悲观锁实现方式有:
- synchronized关键字:synchronized关键字可以作用于方法或代码块,实现对共享资源的加锁;
- ReentrantLock:ReentrantLock是一个可重入锁,可以通过lock()方法获取锁,通过unlock()方法释放锁。
悲观锁的优点是保证数据的一致性,不会出现并发冲突的情况,但缺点是会导致并发性能较低,因为其他事务需要等待锁的释放才能执行。
总结
选择使用乐观锁还是悲观锁,取决于具体的业务场景和性能需求。如果并发冲突较少,并发性能是关键考量因素,那么可以选择乐观锁;如果并发冲突较多,数据一致性是首要考量因素,可以选择悲观锁。当然,也可以根据具体的情况采用两者的结合策略,灵活运用。
代码示例
下面是一个包含乐观锁和悲观锁合在一起实现线程安全的账户转账操作的代码示例,
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private String accountId;
private AtomicInteger balance;
private Lock lock;
public Main(String accountId, int initialBalance) {
this.accountId = accountId;
this.balance = new AtomicInteger(initialBalance);
this.lock = new ReentrantLock();
}
// 使用悲观锁实现转账操作
public void transferPessimistic(Main targetAccount, int amount) {
synchronized (this) {
synchronized (targetAccount) {
if (this.balance.get() >= amount) {
this.balance.addAndGet(-amount); // 当前账户减去转出金额
targetAccount.balance.addAndGet(amount); // 目标账户加上转入金额
System.out.println("悲观锁:账户" + this.accountId + " 向账户" + targetAccount.accountId + " 转账:" + amount);
} else {
System.out.println("悲观锁:余额不足,无法完成转账");
}
}
}
}
// 使用乐观锁实现转账操作
public void transferOptimistic(Main targetAccount, int amount) {
while (true) {
int sourceBalance = this.balance.get(); // 获取当前账户余额
int targetBalance = targetAccount.balance.get(); // 获取目标账户余额
if (sourceBalance >= amount) {
if (this.balance.compareAndSet(sourceBalance, sourceBalance - amount)) { // 原账户减去转出金额
if (targetAccount.balance.compareAndSet(targetBalance, targetBalance + amount)) { // 目标账户加上转入金额
System.out.println("乐观锁:账户" + this.accountId + " 向账户" + targetAccount.accountId + " 转账:" + amount);
break;
}
}
} else {
System.out.println("乐观锁:余额不足,无法完成转账");
break;
}
}
}
public static void main(String[] args) {
Main account1 = new Main("A123", 1000);
Main account2 = new Main("B456", 2000);
// 创建多个线程进行转账操作
Thread thread1 = new Thread(() -> {
account1.transferPessimistic(account2, 300);
});
Thread thread2 = new Thread(() -> {
account2.transferOptimistic(account1, 500);
});
thread1.start();
thread2.start();
}
}
在这个示例中,我们创建了两个账户对象 account1 和 account2,并使用悲观锁和乐观锁来实现了转账操作。transferPessimistic 方法使用悲观锁,在转账过程中加锁了两个账户对象,并根据账户余额判断是否能够完成转账;transferOptimistic 方法使用乐观锁,利用 AtomicInteger 类型的原子操作来保证账户余额的一致性。
在 main 方法中,我们创建了两个线程,并分别调用账户对象的转账方法进行转账操作,模拟了多线程环境下的并发转账。