Java并发编程:深入理解CAS与乐观锁、悲观锁
在Java并发编程中线程安全始终是开发者绕不开的核心话题。而谈到并发控制悲观锁、乐观锁和CAS这三个概念经常被放在一起讨论。它们之间到底是什么关系各自又适用于什么场景今天我们就来彻底讲清楚。一、从两种并发策略说起悲观锁和乐观锁本质上不是某种具体的锁实现而是两种截然不同的并发控制策略。悲观锁Pessimistic Lock悲观锁的核心思想是我总是假设最坏的情况——每次访问共享数据时都认为会有其他线程来修改它。因此在操作数据之前它会先加锁确保数据不会被其他线程干扰。在Java中synchronized关键字和ReentrantLock都是典型的悲观锁实现// synchronized 实现悲观锁publicsynchronizedvoidupdate(){// 需要同步的操作}// ReentrantLock 实现悲观锁privateLocklocknewReentrantLock();lock.lock();try{// 需要同步的操作}finally{lock.unlock();}悲观锁的优点是数据一致性有保障缺点是性能开销大——线程阻塞会导致上下文切换甚至还可能引发死锁问题。乐观锁Optimistic Lock乐观锁则恰恰相反我总是假设最好的情况——认为共享数据被访问时不会发生冲突所以不加锁而是在更新数据时再去验证数据是否被其他线程修改过。乐观锁只是一种思想它的具体实现方式主要有两种版本号机制在数据中增加一个version字段每次更新时版本号加1更新时检查版本号是否与读取时一致。CAS算法即接下来我们要重点讲解的Compare And Swap。乐观锁适用于读多写少的场景省去了加锁和释放锁的开销能显著提升系统吞吐量。但如果冲突频繁乐观锁会导致大量重试反而降低性能。二、CAS —— 乐观锁的核心实现什么是CASCAS全称 Compare And Swap比较并交换是一种无锁原子操作技术。它包含三个操作数· V内存位置要操作的变量· A预期原值期望该变量当前是什么值· B新值想要更新成什么值CAS的执行逻辑是当且仅当内存位置V的值等于预期值A时才将V更新为B否则不做任何操作。用伪代码表示就是booleancompareAndSwap(V,A,B){if(V.valueA){V.valueB;returntrue;}returnfalse;}CAS是乐观锁吗很多人会直接把CAS等同于乐观锁其实更准确的说法是乐观锁是一种思想CAS是这种思想的一种具体实现。CAS完美体现了乐观锁的理念——先大胆去操作不提前加锁只在最后提交时检查是否发生冲突。java.util.concurrent.atomic包下的原子类如AtomicInteger正是通过CAS来实现线程安全的。Java中CAS的底层实现Java中的CAS最终依赖于CPU的原子指令来实现。在Intel CPU上对应的是cmpxchg指令。Java通过sun.misc.Unsafe类提供了对CAS的底层支持publicfinalnativebooleancompareAndSwapInt(Objecto,longoffset,intexpected,intx);这是一个native方法直接调用操作系统的CAS原语。由于CAS是CPU级别的原子操作其执行过程不可被中断因此保证了线程安全性。以AtomicInteger的incrementAndGet()为例publicfinalintincrementAndGet(){for(;;){intcurrentget();intnextcurrent1;if(compareAndSet(current,next))returnnext;}}可以看到它通过自旋不断重试的方式直到CAS操作成功为止。三、三者对比一览对比维度 悲观锁 乐观锁 CAS本质 一种并发策略 一种并发策略 乐观锁的一种实现技术核心思想 先加锁再操作 不加锁更新时检测冲突 比较并交换原子更新典型实现 synchronized、ReentrantLock 版本号机制、CAS AtomicInteger、Unsafe是否阻塞线程 是未获得锁的线程会阻塞 否 否失败线程自旋重试死锁风险 有 无 无天生免疫死锁适用场景 写多读少、冲突频繁 读多写少、冲突较少 低竞争、单变量原子操作四、CAS的三大局限CAS虽然高效但并非银弹它存在三个经典问题ABA问题ABA问题是CAS最著名的缺陷假设共享变量初始值为A线程1读取到A后准备更新期间线程2将值改为B后又改回A。当线程1再次执行CAS时发现值仍然是A于是认为数据没有被修改过但实际上数据已经发生了变化。解决方案使用版本号或时间戳。JDK提供了AtomicStampedReference类通过维护版本号来彻底解决ABA问题。循环时间长开销大CAS操作失败时不会阻塞线程而是通过自旋不断重试。如果竞争非常激烈CAS长时间不成功会导致CPU持续空转带来巨大的性能开销。只能保证单个共享变量的原子操作CAS只能针对一个共享变量进行原子操作。如果要同时更新多个变量就需要把它们封装成一个对象然后使用AtomicReference来保证原子性。五、如何选择悲观锁和乐观锁没有绝对的优劣关键看场景· 写多读少、冲突频繁如金融转账、库存扣减→ 选择悲观锁避免大量重试带来的性能损耗· 读多写少、冲突较少如计数器、配置读取→ 选择乐观锁/CAS省去锁的开销提升吞吐量· 高并发且只涉及单个变量如原子递增→ CAS是非常合适的选择· 临界区代码执行时间长 → 选择悲观锁更合适因为CAS长时间自旋会浪费CPU值得一提的是LongAdder在高并发场景下通过分散竞争的方式将更新分散到多个内部单元降低了CAS重试的概率是在特定场景下对CAS的一种优化。总结悲观锁和乐观锁是两种并发控制的思想而CAS是乐观锁思想在Java中的核心实现技术。理解这三者的区别与联系是写出高性能、高可靠并发代码的基础。在实际开发中没有放之四海而皆准的方案根据业务场景选择合适的并发控制策略才是正确的做法。