一、什么是乐观锁?
乐观锁(Optimistic Locking)是一种并发控制机制,它基于"乐观"的假设:认为数据在大多数情况下不会发生冲突,因此允许多个事务或线程同时访问数据,但在提交更新时会检查数据是否被其他事务修改过。
与悲观锁(Pessimistic Locking)不同,乐观锁不会在数据访问时立即加锁,而是在数据更新时才进行冲突检测。这种机制特别适合读多写少的场景,能够显著提高系统的并发性能。
二、乐观锁的核心思想
乐观锁的核心思想可以概括为以下三点:
- 读取阶段:任何事务都可以自由读取数据
- 修改阶段:事务可以修改读取的数据,并保留原始数据的版本信息
- 提交阶段:在提交修改前,检查数据是否被其他事务修改过
-
- 如果没有修改,则提交成功
- 如果已被修改,则放弃或重试当前操作
三、乐观锁的实现方式
乐观锁通常通过以下两种方式实现:
1. 版本号机制
为数据增加一个版本号字段,每次数据更新时版本号递增。更新时检查版本号是否与读取时一致。
2. CAS(Compare-And-Swap)机制
直接比较数据的当前值是否与读取时的值一致,如果一致则更新。
四、Java中的乐观锁实现示例
1. 基于版本号的乐观锁实现
public class OptimisticLockExample {
// 数据实体类
static class Product {
private int id;
private String name;
private int stock;
private int version; // 版本号字段
// 构造方法、getter和setter省略
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", stock=" + stock +
", version=" + version +
'}';
}
}
// 模拟数据库
static class ProductDatabase {
private static final Map<Integer, Product> db = new HashMap<>();
static {
db.put(1, new Product(1, "手机", 100, 1));
db.put(2, new Product(2, "电脑", 50, 1));
}
public static Product getProduct(int id) {
return db.get(id);
}
public static boolean updateProduct(Product product) {
Product oldProduct = db.get(product.getId());
// 检查版本号是否一致
if (oldProduct.getVersion() != product.getVersion()) {
return false; // 版本不一致,更新失败
}
// 版本一致,更新数据并增加版本号
product.setVersion(product.getVersion() + 1);
db.put(product.getId(), product);
return true;
}
}
// 业务逻辑:减少库存
public static boolean decreaseStock(int productId, int amount) {
while (true) {
Product product = ProductDatabase.getProduct(productId);
if (product == null) {
throw new RuntimeException("产品不存在");
}
int newStock = product.getStock() - amount;
if (newStock < 0) {
throw new RuntimeException("库存不足");
}
// 创建要更新的对象副本
Product newProduct = new Product();
newProduct.setId(product.getId());
newProduct.setName(product.getName());
newProduct.setStock(newStock);
newProduct.setVersion(product.getVersion());
// 尝试更新
if (ProductDatabase.updateProduct(newProduct)) {
System.out.println("更新成功: " + newProduct);
return true;
} else {
System.out.println("更新失败,版本冲突,重试...");
try {
Thread.sleep(100); // 短暂等待后重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
}
public static void main(String[] args) {
// 模拟并发减库存
new Thread(() -> decreaseStock(1, 10)).start();
new Thread(() -> decreaseStock(1, 20)).start();
}
}
2. 基于CAS的乐观锁实现
Java中的原子类(如AtomicInteger)就是基于CAS实现的乐观锁:
import java.util.concurrent.atomic.AtomicInteger;
public class CasOptimisticLockExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// 创建多个线程并发增加计数器
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // 原子增加操作
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // 原子增加操作
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果: " + counter.get()); // 应该是2000
}
}
五、乐观锁的适用场景
- 读多写少的场景:当数据读取远多于修改时,乐观锁能提供更好的性能
- 冲突较少的环境:当并发修改的概率较低时,乐观锁的重试次数少,效率高
- 响应时间要求高的系统:乐观锁不需要等待锁释放,响应更快
- 分布式系统:在分布式环境中实现悲观锁成本较高,乐观锁更合适
六、乐观锁的优缺点
优点:
- 提高并发性能:读操作不会被阻塞,大大提高了系统的吞吐量
- 减少死锁风险:因为没有真正的锁竞争,所以不会产生死锁
- 适合分布式环境:实现相对简单,适合分布式系统
缺点:
- ABA问题:数据可能从A变为B又变回A,版本号机制可以解决这个问题
- 自旋开销:在高并发写场景下,重试次数多会导致CPU资源浪费
- 不保证实时一致性:只能保证最终一致性,不能保证强一致性
七、乐观锁的ABA问题及解决方案
ABA问题是指:
- 线程1读取变量值为A
- 线程2将值改为B,然后又改回A
- 线程1进行CAS操作时发现值仍然是A,认为没有被修改过
解决方案:
- 版本号机制:每次修改都增加版本号,即使值相同版本号也不同
- 时间戳:使用时间戳作为附加条件
- AtomicStampedReference:Java提供的带版本号的引用类
import java.util.concurrent.atomic.AtomicStampedReference;
public class AbaSolutionExample {
private static AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(100, 0);
public static void main(String[] args) {
int stamp = atomicRef.getStamp();
Integer reference = atomicRef.getReference();
// 模拟ABA问题
new Thread(() -> {
atomicRef.compareAndSet(reference, 101, stamp, stamp + 1);
atomicRef.compareAndSet(101, reference, stamp + 1, stamp + 2);
}).start();
new Thread(() -> {
try {
Thread.sleep(100); // 确保第一个线程完成了ABA操作
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean success = atomicRef.compareAndSet(reference, 102, stamp, stamp + 1);
System.out.println("更新是否成功: " + success); // 输出false,因为stamp已经改变
}).start();
}
}
八、乐观锁在实际框架中的应用
1. JPA/Hibernate中的乐观锁
@Entity
public class Product {
@Id
private Long id;
private String name;
private int stock;
@Version // 乐观锁版本号字段
private int version;
// getter和setter
}
2. MyBatis中的乐观锁实现
<update id="updateProduct" parameterType="Product">
UPDATE products
SET stock = #{stock},
version = version + 1
WHERE id = #{id}
AND version = #{version}
</update>
public boolean updateProduct(Product product) {
int affectedRows = productMapper.updateProduct(product);
if (affectedRows == 0) {
throw new OptimisticLockingFailureException("并发修改冲突");
}
return true;
}
九、总结
乐观锁是一种高效的并发控制机制,特别适合读多写少的场景。它通过版本号或CAS机制实现,能够在不阻塞读操作的情况下保证数据的一致性。虽然存在ABA问题和高并发下重试开销等缺点,但通过合理的实现和解决方案,乐观锁仍然是提高系统并发性能的重要工具。