什么是CAS
CAS(Compare and Swap)是一种多线程同步的原子操作,用于解决共享数据的并发访问问题。它允许一个线程尝试修改共享变量的值,但只有在变量的当前值与预期值匹配的情况下才会执行更新操作。
CAS操作包括三个主要步骤:
比较(Compare):线程首先读取共享变量的当前值,这个值通常是期望的值。
比较预期值:线程将当前值与预期的值进行比较。如果它们匹配,表示变量的当前值与线程期望的值相同。
更新(Swap):如果比较成功,线程执行更新操作,将变量的新值写入共享内存。否则,如果比较失败,线程不执行任何更新操作。
原子性(Atomicity):CAS操作是原子性的,即在执行比较和更新的整个过程中,其他线程无法中断或插入。这确保了操作的一致性。
CAS操作通常用于解决多线程并发访问共享变量时的同步问题。它允许一个线程在不需要锁的情况下,以原子的方式对共享变量进行修改。CAS是一种乐观锁(Optimistic Locking)的实现方式,它允许多个线程同时尝试修改一个变量,但只有一个线程会成功,其他线程需要重试或处理失败情况。
CAS的作用
CAS的主要作用是确保多个线程对共享变量的并发访问是线程安全的。
CAS用于代替传统锁机制,减少锁带来的性能开销和竞争,特别在高并发情况下具有显著的性能优势。
CAS避免了锁可能引发的死锁问题,因为它是一种乐观锁(Optimistic Locking)的实现方式,允许多个线程同时尝试修改变量,但只有一个线程会成功。
CAS可以实现原子操作,因此可用于实现诸如计数器递增、标志位的设置、线程安全队列的操作等。
示例
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class CASExample {
private static final Unsafe unsafe;
private volatile int value = 0;
private static long valueOffset;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
valueOffset = unsafe.objectFieldOffset(CASExample.class.getDeclaredField("value"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public int getValue() {
return value;
}
public void increment() {
int oldValue, newValue;
do {
oldValue = value;
newValue = oldValue + 1;
//当 this中的value 和oldValue相同的时候将value更新为 newValue
} while (!unsafe.compareAndSwapInt(this, valueOffset, oldValue, newValue));
}
public static void main(String[] args) {
CASExample counter = new CASExample();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.increment();
}
});
thread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Count: " + counter.getValue());
}
}
CAS优势和局限性
优点
无锁编程:CAS操作不需要使用传统的锁机制,因此减少了锁带来的性能开销和竞争。
高性能:CAS是一种轻量级的同步机制,通常比锁具有更好的性能表现。
避免死锁:CAS操作避免了传统锁可能引发的死锁问题。
并发性:CAS允许多个线程同时尝试修改共享变量,以提高并发性。
注意事项
ABA问题:CAS可能受到ABA问题的影响,其中一个线程可能在共享变量值从A变为B再变回A时执行成功,尽管中间的状态变化可能引发问题。
自旋等待:CAS操作可能需要多次尝试才能成功,这会消耗一定的CPU资源。因此,需要设定一个最大尝试次数或者超时时间来避免无限自旋。
不适用于所有情况:CAS适用于特定类型的原子操作,但不适用于所有并发问题。
并发性:CAS操作的并发性较高,但在高并发情况下,可能会出现多个线程竞争同一个内存位置,从而导致CAS操作的失败率上升。
ABA问题与解决方案
什么是ABA问题
ABA问题是一种在并发编程中常见的问题,它涉及到CAS(Compare and Swap)操作。ABA问题的核心是在一个线程尝试修改共享变量时,共享变量的值从A变为B,然后再变回A。这种情况可能导致CAS操作成功,尽管在中间发生了其他操作,从而引发意外的行为。
具体来说,ABA问题的情况如下:
线程T1读取共享变量的值A,并保存在本地。
在此期间,线程T2修改共享变量的值,将其从A改为B,然后再改回A。
线程T1尝试使用CAS操作将共享变量的值从A改为新值C。CAS操作成功,因为共享变量的当前值是A,与预期值A相匹配。
从CAS操作的角度来看,操作是成功的,因为共享变量的值从A变为C,尽管中间发生了A到B再到A的变化。这种情况可能在一些情况下引发问题,特别是在需要确保操作的一致性和准确性的情况下。
解决ABA问题的方案
版本号或标记:为共享变量引入版本号或标记,以跟踪变量的状态。这样,即使值从A到B再到A,版本号或标记会随之增加,CAS操作会检查版本号或标记是否匹配。
AtomicStampedReference:Java提供了AtomicStampedReference类,它允许在CAS操作中包括一个额外的整数,以跟踪变量的版本或标记。
使用锁:在某些情况下,使用传统的锁机制可以避免ABA问题。锁机制会确保一次只有一个线程可以修改共享变量。
ABA解决问题案例
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABAExample {
public static void main(String[] args) {
// 创建一个AtomicReference,用于模拟不带版本号的CAS
AtomicReference<Integer> atomicRef = new AtomicReference<>(100);
// 创建一个AtomicStampedReference,用于模拟带版本号的CAS
AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(100, 0);
// 创建线程1,尝试进行CAS操作
Thread thread1 = new Thread(() -> {
int newValue = 101;
// 使用AtomicReference进行CAS操作,更新值
atomicRef.compareAndSet(100, newValue);
// 使用AtomicStampedReference进行CAS操作,更新值并版本号加1
stampedRef.compareAndSet(100, newValue, 0, 1);
System.out.println("Thread 1: Value is updated to " + newValue);
});
// 创建线程2,模拟中间有其他线程修改过值
Thread thread2 = new Thread(() -> {
int newValue = 102;
// 模拟中间有其他线程修改过值,使用AtomicReference将值设为99
atomicRef.compareAndSet(100, 99);
// 模拟中间有其他线程修改过值,使用AtomicStampedReference将值设为99,并版本号加1
stampedRef.compareAndSet(100, 99, 0, 1);
System.out.println("Thread 2: Value is updated to 99");
// 再将值改回来,如果版本号匹配,CAS操作成功
boolean success = atomicRef.compareAndSet(99, 100);
boolean stampedSuccess = stampedRef.compareAndSet(99, 100, 1, 2);
System.out.println("Thread 2: Value is updated back to 100: " + success);
System.out.println("Thread 2 (Stamped): Value is updated back to 100: " + stampedSuccess);
});
// 启动线程1和线程2
thread1.start();
thread2.start();
// 等待线程1和线程2执行完成
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终的值
System.out.println("Final Value (AtomicReference): " + atomicRef.get());
System.out.println("Final Value (AtomicStampedReference): " + stampedRef.getReference());
}
}
在这个示例中,我们创建了两个线程,thread1和thread2。thread1首先尝试使用AtomicReference和AtomicStampedReference执行CAS操作,将值从100更新为101。然后,thread2模拟中间有其他线程修改过值,使用相同的方法将值设为99,然后将值再次修改回100。
CAS原理
1.读取内存位置的当前值。
2.检查当前值是否等于期望值。
3.如果相等,将内存位置的值更新为新值。
4.如果不相等,不做任何操作,可以重试或执行其他操作。
CAS与锁的对比
并发性:
CAS具有较高的并发性,因为多个线程可以同时尝试执行CAS操作,不会阻塞其他线程。
锁的并发性较低,因为只有一个线程能够获得锁,其他线程必须等待。
自旋:
CAS可能需要自旋(即多次尝试)来尝试成功,这可能会导致一定的CPU消耗。
锁使用了阻塞机制,当线程无法获得锁时,会被挂起,不会消耗CPU资源。
ABA问题:
CAS可能存在ABA问题,即共享数据的值在操作过程中被其他线程改变回原始值,导致CAS操作成功,但实际数据已经发生变化。
锁不容易出现ABA问题,因为它们在获得锁时会等待,直到获得锁后再执行操作。
适用性:
CAS适用于需要高并发性和较小粒度的数据更新场景,如原子变量的更新。
锁适用于复杂的临界区保护和需要确保一组操作的原子性的场景。
性能:
CAS在低冲突情况下具有较高的性能,因为它允许多线程并发地进行操作。
锁在高冲突情况下可能具有更好的性能,因为它能够协调线程的执行顺序,避免争用。