摘要:作为互联网大厂的后端开发人员,在多线程编程的场景下,你是不是经常会用到 CAS(Compare and Swap,比较与交换)机制来实现并发控制?它基于硬件级别的原子操作,高效又便捷,能够在不使用锁的情况下,完成数据的更新操作,极大提升程序性能。但是,当你使用
作为互联网大厂的后端开发人员,在多线程编程的场景下,你是不是经常会用到 CAS(Compare and Swap,比较与交换)机制来实现并发控制?它基于硬件级别的原子操作,高效又便捷,能够在不使用锁的情况下,完成数据的更新操作,极大提升程序性能。但是,当你使用 CAS 时,有没有遇到过看似数据正确更新了,程序却出现了奇怪的逻辑错误?很有可能,你已经掉进了 CAS 中 ABA 问题的 “陷阱”!
CAS 机制原理
CAS 是一种乐观锁策略,它有三个操作数:内存值 V、旧的预期值 A、要修改的新值 B 。只有当内存值 V 和预期值 A 相等时,才会将内存值 V 更新为 B,否则不做任何操作。在多线程环境下,线程之间的执行顺序具有不确定性,这就为 ABA 问题埋下了隐患。
ABA 问题本质
简单来说,ABA 问题就是一个值从 A 变成 B,又变回 A ,此时如果有线程执行 CAS 操作,它会误以为数据没有被修改过,从而导致 CAS 操作成功,但实际上数据已经经历了中间变化。
JVM 底层实现角度剖析
从 JVM 底层实现角度来看,在 HotSpot 虚拟机中,CAS 操作是通过cmpxchg指令实现的,该指令在不同的 CPU 架构上有着不同的实现方式 。以 x86 架构为例,cmpxchg指令在执行时会自动锁定总线,保证操作的原子性。然而这种原子性仅仅保证了值的比较和交换过程,无法感知值的变化历史。
实际应用中的危害
在实际应用中,ABA 问题带来的危害不容小觑。在一个电商库存系统中,库存数量初始值为 100(A)。线程 1 读取到库存数量为 100,准备将库存减少 1 个。就在这时,线程 2 抢先一步,将库存改为 99(B),并完成了一次商品出库操作。紧接着,另一个线程 3 又将库存数量补回了 100(变回 A) 。这时候线程 1 执行 CAS 操作,发现当前库存值还是 100,符合预期,就将库存更新为 99。但实际上,这一次库存的减少,并不是正常的商品出库,而是因为中间数据的变化导致的错误更新,这会对库存系统的数据准确性产生严重影响。
不仅如此,在分布式系统的分布式锁、缓存数据一致性等场景中,ABA 问题同样可能导致严重的系统故障。比如在分布式锁实现里,如果发生 ABA 问题,可能会导致锁被错误释放,进而引发多个线程同时访问临界资源,造成数据混乱。
使用 AtomicStampedReference
它在 AtomicReference 的基础上,引入了一个时间戳(stamp)。每次数据发生变化时,时间戳都会相应增加。在执行 CAS 操作时,不仅要比较数据值,还要比较时间戳。只有当数据值和时间戳都符合预期时,CAS 操作才会成功。
从源码层面分析,AtomicStampedReference的compareAndSet方法会同时对值和时间戳进行比较更新,其内部维护了一个Pair对象,用于存储值和时间戳。还是以刚才的库存系统为例,引入 AtomicStampedReference 后,线程 1 读取库存数量 100 时,会同时获取到对应的时间戳,假设为 1。当线程 2 将库存改为 99 时,时间戳变为 2;线程 3 再改回 100 时,时间戳变为 3。此时线程 1 执行 CAS 操作,由于预期的时间戳是 1,而实际的时间戳是 3,不匹配,CAS 操作就会失败,从而避免了 ABA 问题带来的错误更新。
不过,使用AtomicStampedReference也有一定的性能开销,因为每次操作都需要额外处理时间戳,在高并发场景下,可能会对系统性能有一定影响。
使用 AtomicMarkableReference
它通过一个布尔值来标记数据是否被修改过。在执行 CAS 操作时,会同时比较数据值和标记值。如果数据值和标记值都符合预期,才会执行更新操作。
这种方式相对 AtomicStampedReference 来说,实现更加简单,适用于一些对标记信息要求不那么精确的场景。例如在一些简单的状态标记场景中,只需要知道数据是否被修改过,而不需要记录详细的修改次数和顺序,AtomicMarkableReference就能很好地满足需求 。它的源码实现中,compareAndSet方法同样会同时处理值和标记,不过相对AtomicStampedReference,逻辑更为简洁。
但需要注意的是,由于它只有一个布尔标记,在复杂的多线程操作场景下,可能无法提供足够详细的信息来完全避免数据不一致问题。
版本号机制
除了 JDK 提供的AtomicStampedReference和AtomicMarkableReference,我们还可以自定义版本号机制。在数据库表中添加一个版本号字段,每次对数据进行修改时,版本号自增。在执行 CAS 操作时,不仅比较数据值,还比较版本号。只有当版本号符合预期时,才执行更新操作。
这种方式在分布式系统中尤为适用,例如在基于数据库实现的分布式锁中,通过版本号机制可以有效避免 ABA 问题,保证锁的安全性和一致性。不过,这种方式需要额外的数据库字段维护,会增加一定的数据库操作开销。
在互联网大厂的后端开发工作中,多线程并发控制是一个非常重要的领域,而 CAS 作为一种高效的无锁机制,应用十分广泛。但是,ABA 问题就像是隐藏在 CAS 背后的 “暗雷”,稍有不慎就会引发程序的逻辑错误,影响系统的稳定性和数据准确性。从 JVM 底层指令实现,到实际业务场景中的各种隐患,再到多种解决方案的优劣对比,每一个环节都需要我们深入理解和掌握。
希望通过今天的分享,大家能够对 CAS 中的 ABA 问题有更深入的理解,在以后的开发工作中,根据实际场景,合理选择解决方案,避免踩坑。如果你在实际开发中也遇到过类似的问题,或者有更好的解决思路,欢迎在评论区留言讨论,让我们一起把后端开发技术 “玩” 得更溜!
来源:从程序员到架构师一点号