Java并发编程:乐观锁与悲观锁深度解析
在多线程和高并发编程中数据的一致性和线程安全是核心挑战。为了保证共享资源在并发访问下的正确性我们需要引入锁机制。Java中的锁主要分为两大类悲观锁和乐观锁。本教程将带你深入理解这两种锁的思想、实现原理、代码实战以及应用场景。一、核心思想悲观与乐观的博弈理解这两种锁最简单的方式是从它们的“心态”入手。悲观锁未雨绸缪的“安全卫士”心态它假设最坏的情况认为每次去访问共享资源时一定会发生冲突。行为因此它在操作数据之前会先上锁。在锁被释放之前其他线程必须等待。类比你去图书馆借一本热门的书。悲观锁的做法是你一进入图书馆就先把这本书抱在怀里并且锁上柜门。在你办理完借阅手续离开之前其他人连书都摸不到。乐观锁轻装上阵的“高效玩家”心态它假设最好的情况认为数据一般情况下不会造成冲突。行为因此它在读取数据时不加锁只有在真正提交更新时才会检查数据在此期间是否被别人修改过。如果发现冲突则采取重试或报错等措施。类比同样是借那本热门的书。乐观锁的做法是你先把书拿到阅读区看准备去结账时才检查这本书是否已经被别人借走了。如果没被借走你就成功借阅如果被借走了你就需要重新选择或者等待。二、悲观锁实战悲观锁的核心是“阻塞”通过强制串行化来保证数据安全。Java中的实现synchronized关键字这是Java中最常用的悲观锁实现。它会自动地获取和释放锁。public class PessimisticCounter { private int count 0; // synchronized修饰实例方法锁的是当前实例对象(this) public synchronized void increment() { count; } public int getCount() { return count; } }ReentrantLock类这是一个显式的锁提供了比synchronized更灵活的功能如可中断、可超时、公平锁等。import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class PessimisticCounterWithLock { private int count 0; private final Lock lock new ReentrantLock(); public void increment() { lock.lock(); // 手动加锁 try { count; } finally { lock.unlock(); // 必须在finally块中释放锁防止死锁 } } public int getCount() { return count; } }数据库中的实现在数据库层面悲观锁通常通过SELECT ... FOR UPDATE语句实现。这会在查询的同时给选中的行加上排他锁其他事务必须等待当前事务提交后才能修改该行。-- 开启事务 BEGIN; -- 1. 查询并锁定id为1的记录 SELECT stock FROM goods WHERE id 1 FOR UPDATE; -- 2. 业务逻辑判断库存充足后扣减 UPDATE goods SET stock stock - 1 WHERE id 1; -- 3. 提交事务释放锁 COMMIT;优缺点分析优点实现简单逻辑清晰。数据安全性高能完全避免并发冲突。缺点性能开销大线程阻塞和上下文切换会带来性能损耗尤其是在高并发场景下可能导致大量线程等待吞吐量下降。可能引发死锁如果多个线程以不同的顺序获取多个锁就可能发生死锁。适用场景写操作频繁数据竞争激烈的场景如电商秒杀扣库存。对数据一致性要求极高宁可牺牲性能也要保证数据绝对正确的场景如金融系统的转账操作。三、乐观锁实战乐观锁的核心是“无锁”和“冲突检测”通过非阻塞的方式提升并发性能。它主要有两种实现方式版本号机制和CAS。实现方式一版本号机制这是最通用、最易于理解的乐观锁实现尤其在数据库操作中。原理在数据表中增加一个version字段。读取数据时同时读取当前的version值。更新数据时将version值加1并在WHERE条件中检查version是否与读取时一致。如果一致说明数据未被修改更新成功。如果不一致说明数据已被其他线程修改更新失败需要重试或报错。数据库SQL示例-- 1. 查询数据同时获取版本号 SELECT stock, version FROM goods WHERE id 1; -- 假设查出来 version 1 -- 2. 更新数据检查版本号 UPDATE goods SET stock stock - 1, version version 1 WHERE id 1 AND version 1; -- 只有version仍为1时更新才会成功Java代码模拟public class OptimisticCounter { private int count 0; private int version 0; // 模拟一次更新操作成功返回true失败返回false public synchronized boolean increment() { int expectedVersion this.version; // 1. 记录当前版本号 int expectedCount this.count; // 2. 记录当前数据值 // 模拟业务逻辑处理... int newCount expectedCount 1; // 3. 提交更新时检查版本号是否发生变化 if (expectedVersion this.version) { this.count newCount; this.version; // 4. 更新数据和版本号 return true; } else { return false; // 5. 版本号已变更新失败 } } }实现方式二CASCompare-And-SwapCAS是乐观锁在硬件层面的原子实现是Java并发包JUC的基石。原理CAS操作包含三个操作数内存位置V需要更新的变量。预期原值A线程读取到的变量值。新值B线程希望更新的新值。CAS指令会原子性地执行以下逻辑如果内存位置V的值等于预期原值A则将V的值更新为新值B否则不做任何操作。无论哪种情况它都会返回V的当前值。Java中的java.util.concurrent.atomic包下的原子类如AtomicInteger就是基于CAS实现的。代码示例使用AtomicIntegerimport java.util.concurrent.atomic.AtomicInteger; public class CASCounter { // 使用AtomicInteger其内部的incrementAndGet()方法就是基于CAS private AtomicInteger count new AtomicInteger(0); public void increment() { // 这是一个原子操作内部会不断重试直到成功 count.incrementAndGet(); } public int getCount() { return count.get(); } }手动实现CAS逻辑自旋public class ManualCASCounter { private AtomicInteger count new AtomicInteger(0); public void increment() { int oldValue; int newValue; do { // 1. 读取当前值 oldValue count.get(); // 2. 计算新值 newValue oldValue 1; // 3. 尝试用CAS更新如果失败返回false则循环重试自旋 } while (!count.compareAndSet(oldValue, newValue)); } }乐观锁的优缺点分析优点性能高无锁操作避免了线程阻塞和上下文切换吞吐量高。不会死锁。缺点ABA问题这是CAS的经典缺陷。高冲突下性能差如果并发写非常频繁会导致大量线程更新失败并不断重试自旋这会严重消耗CPU资源性能反而可能低于悲观锁。实现复杂需要处理版本号和重试逻辑。ABA问题详解问题描述线程T1读取变量值为A。T1被挂起线程T2将变量值从A改为B然后又改回A。T1恢复执行执行CAS时发现内存值依旧是A判定无修改成功更新。问题变量实际已经被修改过CAS无法感知可能导致业务逻辑错误。解决方案给变量增加一个版本号Stamp。每次修改时不仅更新值也更新版本号。这样即使值从A变回A版本号也一定不同。Java提供了AtomicStampedReference类来解决此问题。适用场景读多写少的场景数据竞争不激烈如文章点赞数统计、缓存更新。高并发下追求高吞吐量的场景。四、总结与选型指南对比维度悲观锁乐观锁核心思想假设冲突一定发生操作前先加锁假设冲突很少发生提交时检查冲突实现方式synchronized,ReentrantLock,SELECT ... FOR UPDATE版本号机制, CAS原子操作线程行为阻塞等待非阻塞失败后重试自旋性能开销高线程阻塞、上下文切换低无阻塞但高冲突时CPU开销大数据一致性强一致最终一致依赖重试适用场景写多读少竞争激烈强一致性要求读多写少竞争不激烈高吞吐量要求一句话选型建议冲突多用悲观锁冲突少用乐观锁。在实际开发中应根据具体的业务场景、并发量和数据一致性要求来综合权衡选择最合适的锁机制。