什么是 CAS ?
CAS 全称 Compare And Swap,翻译为中文是比较并交换,是一种无锁的原子操作,CAS 可以不使用锁来保证多线程修改数据的安全性,虽说是无锁但实际上使用了一种乐观锁的思想,也可以认为 CAS 是乐观锁的一种实现。
CAS 的原理?
CAS 原理是依赖于硬件支持的原子性指令来实现的, CAS 在 Java 中的实现是由 Unsafe 类来实现的,Unsafe 类是 JDK 提供的一个不安全的类,它提供了一些底层的操作,它的作用是让 Java 可以在底层直接操作内存,从而提高程序的效率,Unsafe 类中就提供了关于 CAS 的方法。
CAS 的作用?
CAS 可以实现无锁情况的下的原子操作,当多线程在修改同一个共享变量的时候,只要一个线程可以更新成功,其他线程会更新失败,失败的线程不会被挂起(这里就节省了线程挂起和唤醒的开销),还可以继续尝试修改,使用 CAS 就实现了 synchronized、ReentrantLock 等锁的同样效果,但开销会更小,因此,CAS操作广泛应用于并发编程中,例如我们熟知的 AbstractQueuedSynchronizer、ReentrantLock 等都广泛使用了 CAS 。
CAS 的用法?
CAS 操作主要由三个参数,分别是要更新的参数、期望值、新值,CAS 操作会先获取共享变量在内存中的值与期望值进行比较,如果相同,则把变量设置为新值,否则就修改失败,可以重新进行 CAS 操作。
CAS 的优缺点?
- CAS 依赖于硬件层面的原子指令,实现无锁并发,省去了线程加锁解锁的开销。
- CAS 会出现 ABA 问题。
- CAS 自旋如果不成功,会给 CPU 带来较大的开销,因此 CAS 不适合使用在竞争很大的场景。
- CAS 只能保证单个变量操作的原子性,有一定使用局限性,而如 synchronized、ReentrantLock 等锁就没有这个问题。
什么是 CAS 的 ABA?
ABA 问题简单来说就是一个变量初始值为 A,被修改成 B,然后又被修改成 A,CAS 是无法识别到这个过程的。
ABA 流程演示:
分析 ABA 流程:
- 线程1 读取到共享变量 str 的初始值为 A,准备执行 CAS(A,B) 操作。
- 此时线程2 抢占了 CPU 时间片,读取到共享变量 str 的初始只为 A,执行 CAS(A,B) 操作。
- 接着线程2 继续执行 CAS(B,A) 操作,此时共享变量 str 的值为 A,线程2 释放了 CPU 时间片。
- 线程1 终于抢回了 CPU 时间片,继续执行 CAS(A,B) 操作,执行成功结束。
整个流程有个很明显的问题,享变量 str 被线程2 修改为 B,然后再次修改回 A,而线程1 没有察觉到,还是正常的执行了 CAS 操作。
怎么解决 ABA 问题?
ABA问题最简单的解决方案就是使用版本号,在每次修改数据时都携带一个版本号,只有当该版本号与数据的版本号一致时,才能执行数据的修改,否则修改失败,因为 CAS 操作时携带了版本号,而版本号在每次修改时都会递增,并且只会增加不会减少,不会出现版本号一直的问题,也就有效的避免了 ABA 问题。
Java 中如何解决 ABA 问题?
Java 中使用了时间错来解决 CAS ABA 问题,其实也是版本号的思想,Java 提供了AtomicStampedReference 和 AtomicMarkableReference 来解决 ABA 问题,这两个类在 CAS 的基础上加了一个时间戳,也就是版本号,时间戳是递增的,,,。
AtomicStampedReference 类 weakCompareAndSet 方法源码分析:
public boolean weakCompareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {return compareAndSet(expectedReference, newReference,expectedStamp, newStamp);}//expectedReference 期望的值//newReference 更新的值//expectedStamp 期望的时间戳//newStamp 更新后的时间戳public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {Pair<V> current = pair;//只有当前值等于期望值 且当前时间戳等于期望时间戳 才会执行 CAS 操作returnexpectedReference == current.reference &&expectedStamp == current.stamp &&((newReference == current.reference &&newStamp == current.stamp) ||casPair(current, Pair.of(newReference, newStamp)));}
根据源码分析可知,AtomicStampedReference 类 weakCompareAndSet 方法只关心期望值和当前值是否相同,并不会关注是否被修改过,而 AtomicMarkableRederence 类的 weakCompareAndSet 方法会关注是否被修改过,下面我来分析一下。
AtomicMarkableRederence 类的 weakCompareAndSet 方法源码分析:
public boolean weakCompareAndSet(V expectedReference,V newReference,boolean expectedMark,boolean newMark) {return compareAndSet(expectedReference, newReference,expectedMark, newMark);}//expectedReference 期望的值
//newReference 更新的值
//expectedStamp 期望的标识
//newStamp 更新后的标识
public boolean compareAndSet(V expectedReference,V newReference,boolean expectedMark,boolean newMark) {Pair<V> current = pair;returnexpectedReference == current.reference &&expectedMark == current.mark &&((newReference == current.reference &&newMark == current.mark) ||casPair(current, Pair.of(newReference, newMark)));}
根据源码分析可知, AtomicMarkableRederence 类的 weakCompareAndSet 方法没有时间戳入参,只有 boolean 类型的修改表标识入参,用来标记变量是否被修改过。
CAS 应用场景:
- 数据库并发控制,乐观锁就是通过 CAS 思想来实现的,它可以在数据库并发控制中保证多个事务同时访问同一数据时的一致性。
- 自旋锁,自旋锁是一种非阻塞锁,当线程尝试获取锁时,如果锁已经被其他线程占用,则线程不会进入休眠,而是一直在自旋等待锁的释放,AbstractQueuedSynchronizer 中就有大量使用。
- 线程安全计数器,由于CAS操作是原子性的,可以使用 CAS 设计实现一个线程安全的计数器;。
- 队列,在并发编程中,队列经常用于多线程之间的数据交换,AbstractQueuedSynchronizer 的 CHL 队列中节点状态变更、节点的变更就大量使用了 CAS 操作。
CAS 在并发编程中使用广泛,值得我们深入研究,有利用我们更好的理解一些底层框架源码。
如有错误的地方欢迎指出纠正。