0%

Java CAS

简介

CAS (Compare And Swap) 比较并交换, CAS 算法 CAS(V,E,N) 包含 3 个参数, V 表示要更新的变量, E 表示预期的值, N 表示新值, 在且仅在 V 值等于 E 值时, 才会将 V 值设为 N, 如果 V 值和 E 值不同, 则说明已经有其他线程做了更新, 当前线程什么都不做, 最后, CAS 返回当前 V 的真实值

特点

CAS 操作采用了乐观锁的思想, 多个线程同时使用 CAS 操作一个变量时, 只有一个会成功更新, 其余均会失败, 失败的线程不会被挂起, 仅被告知失败, 并且允许再次尝试, 也允许失败的线程放弃操作

Atomic

在 JDK 的原子包 java.util.concurrent.atomic 里面提供了一组原子类, 这些原子类的基本特性就是在多线程环境下, 在有多个线程同时执行这些类的实例包含的方法时, 会有排他性, 其内部便是基于 CAS 算法实现的, 即在某个线程进入方法中执行其中的指令时, 不会被其他线程打断;而别的线程就像自旋锁一样, 一直等到该方法执行完成才由 JVM 从等待的队列中选择另一个线程进入

AtomicInteger 中 getAndIncrement 方法采用了 CAS 操作, 每次都从内存中读取数据然后将此数据和加 1 后的结果进行 CAS 操作, 如果成功, 则返回结果, 否则 CAS 自旋重试直到成功为止

与 synchronized 对比

相对于 synchronized 阻塞算法, CAS 是非阻塞算法的一种常见实现, 由于 CPU 的切换比 CPU 指令集的操作更加耗时, 所以 CAS 的自旋操作在性能上有了很大的提升

ABA 问题

对 CAS 算法的实现有一个重要的前提, 需要取出内存中某时刻的数据, 然后在下一时刻进行比较, 替换, 在这个时间差内可能数据已经发生了变化, 导致产生 ABA 问题

ABA 问题指第 1 个线程从内存的 V 位置取出 A, 这时第 2 个线程也从内存中取出 A, 并将 V 位置的数据首先修改为 B, 接着又将 V 位置的数据修改为 A, 这时第 1 个线程在进行 CAS 操作时会发现在内存中仍然是 A, 然后第 1 个线程操作成功, 尽管从第 1 个线程的角度来说, CAS 操作是成功的, 但在该过程中其实 V 位置的数据发生了变化, 只是第 1 个线程没有感知到罢了, 这在某些应用场景下可能出现过程数据不一致的问题

部分乐观锁是通过版本号 (version) 来解决 ABA 问题的, 具体的操作是乐观锁每次在执行数据的修改操作时都会带上一个版本号, 在预期的版本号和数据的版本号一致时就可以执行修改操作, 并对版本号执行加 1 操作, 否则执行失败, 因为每次操作的版本号都会随之增加, 所以不会出现 ABA 问题, 因为版本号只会增加, 不会减少