CAS
# CAS比较并交换(compare and swap)
Java中的CAS是"Compare and Swap"(比较并交换)的缩写,它是一种多线程编程中用于实现同步操作的技术。CAS操作通常用于实现原子操作,这意味着在多线程环境下,它可以保证某个操作以原子方式执行,不会被其他线程中断或干扰。
CAS操作包括三个步骤:
- 比较:首先,CAS会比较一个变量的当前值与一个期望值。
- 交换:如果当前值等于期望值,CAS会尝试用新值替换当前值。这一步是原子的,意味着如果其他线程在此期间修改了该变量的值,CAS操作将失败。
- 返回结果:CAS操作会返回替换前的旧值,以便调用者可以检查是否操作成功。
CAS操作常用于实现锁、并发数据结构、线程安全的计数器等。Java中的java.util.concurrent
包提供了Atomic
系列的类,例如AtomicInteger
,它们使用CAS操作来提供线程安全的操作。这些类通常比使用传统锁的方式更高效,因为它们允许多个线程并发地执行操作而不需要显式的锁定整个数据结构。
# 举例说明CAS的使用:
当涉及多线程并发场景时,CAS(Compare and Swap)操作常常用于确保对共享资源的原子性操作。一个典型的例子是使用CAS来实现一个简单的自旋锁。
假设我们有一个共享的计数器,多个线程需要对计数器进行递增操作,但我们希望在进行递增操作时能够确保原子性。这时就可以使用CAS操作来实现一个简单的自旋锁,代码示例如下(Java语言):
javaCopy Codeimport java.util.concurrent.atomic.AtomicInteger;
public class SpinLockExample {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
int expect;
int update;
do {
expect = counter.get(); // 获取当前值作为预期值
update = expect + 1; // 新值为预期值加1
} while (!counter.compareAndSet(expect, update)); // CAS操作,如果失败则继续循环
// 在CAS操作成功后,表示计数器已经成功递增
}
}
在上述例子中,increment()方法使用了CAS操作来实现对计数器的递增操作。首先获取当前值作为预期值,然后根据业务逻辑设定新的值,最后使用compareAndSet()方法进行CAS操作,如果操作失败则继续循环直到CAS操作成功。
这样就能够确保在多线程并发访问时,对计数器的递增操作能够保持原子性,避免了使用传统锁机制可能带来的性能开销和死锁等问题。
# AtomicInteger原子类
AtomicInteger等原子类没有使用synchronized锁,而是通过volatile和CAS(Compare And Swap)解决资源的线程安全问题。
(1)volatile保证了可见性和有序性
(2)CAS保证了原子性,而且是无锁操作,提高了并发效率。
//创建Unsafe类的实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//成员变量value是在内存地址中距离当前对象首地址的偏移量, 具体赋值是在下面的静态代码块中中进行的
private static final long valueOffset;
static {
try {
//类加载的时候,在静态代码块中获取变量value的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex); }
}
// 当前AtomicInteger原子类的value值
private volatile int value;
//类似于i++操作
public final int getAndIncrement() {
//this代表当前AtomicInteger类型的对象,valueOffset表示value成员变量的偏移量
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe类中的方法
//此方法的作用:获取内存地址为原子对象首地址+原子对象value属性地址偏移量, 并将该变量值加上delta
public final int getAndAddInt(Object obj, long offset, int delta) {
int v;
do {
//通过对象和偏移量获取变量值作为期望值,在修改该内存偏移位置的值时与原始进行比较
//此方法中采用volatile的底层原理,保证了内存可见性,所有线程都从内存中获取变量vlaue的值,所有线程看到的值一致。
v= this.getIntVolatile(obj, offset);
//while中的compareAndSwapInt()方法尝试修改v的值,具体地, 该方法也会通过obj和offset获取变量的值
//如果这个值和v不一样, 说明其他线程修改了obj+offset地址处的值, 此时compareAndSwapInt()返回false, 继续循环
//如果这个值和v一样, 说明没有其他线程修改obj+offset地址处的值, 此时可以将obj+offset地址处的值改为v+delta,
//compareAndSwapInt()返回true, 退出循环
//Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被其他线程中断
} while(!this.compareAndSwapInt(obj, offset, v, v + delta));
return v;
}
操作步骤:
- 获取AtomicInteger对象首地址指定偏移量位置上的值,作为期望值。
- 取出获取AtomicInteger对象偏移量上的值,判断与期望值是否相等,相等就修改AtomicInteger在内存偏移量上的值,不相等就返回false,重新执行第一步操作,重新获取内存指定偏移量位置的值。
- 如果相等,则修改值并返回true。 注意:从1、2步可以看CAS机制实现的锁是自旋锁,如果线程一直无法获取到锁,则一直自旋,不会阻塞
# CAS和syncronized的比较
CAS线程不会阻塞,线程一致自旋 syncronized会阻塞线程,会进行线程的上下文切换,会由用户态切换到内核态,切换前需要保存用户态的上下文,而内核态恢复到用户态,又需要恢复保存的上下文,非常消耗资源。
# CAS的缺点
(1)ABA问题 如果一个线程t1正修改共享变量的值A,但还没修改,此时另一个线程t2获取到CPU时间片,将共享变量的值A修改为B,然后又修改为A,此时线程t1检查发现共享变量的值没有发生变化,但是实际上却变化了。
解决办法: 使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JUC包里提供了一个类AtomicStampedReference来解决ABA问题。
AtomicStampedReference类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
(2)循环时间长开销会比较大:自旋重试时间,会给CPU带来非常大的执行开销
(3)只能保证一个共享变量的原子操作,不能保证同时对多个变量的原子性操作
解决办法: 从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作
# CAS使用注意事项
(1)CAS需要和volatile配合使用
CAS只能保证变量的原子性,不能保证变量的内存可见性。CAS获取共享变量的值时,需要和volatile配合使用,来保证共享变量的可见性
(2)CAS适用于并发量不高、多核CPU的情况
CPU多核情况下可以同时执行,如果不合适就失败。而并发量过高,会导致自旋重试耗费大量的CPU资源