目录
1. CAS简述
在Java中,CAS(Compare And Swap)中文翻译成比较并交换是一种重要的并发编程技术,它是Java并发包(java.util.concurrent,简称JUC)中许多同步类实现的基础。CAS操作包含三个操作数——内存位置(V),预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,就将内存位置的值修改为新值。这个操作是原子的,即线程在执行这个操作时不会被其他线程中断。
CAS操作的基本原理
CAS操作涉及到以下三个操作数:
- 内存位置(V):一个内存地址,它存储着我们要修改的数据。
- 预期值(A):我们假设这个内存位置的当前值是A。
- 新值(B):如果内存位置的值确实是A,我们希望更新为B。
CAS操作执行时,会做以下几步:
- 读取:从指定的内存位置读取值。
- 比较:比较读取到的值和预期值A是否相等。
- 交换(如果需要):如果步骤2中的比较结果为真(即读取的值等于预期值A),则将新值B写入该内存位置。如果比较结果为假(即读取的值不等于预期值A),则不进行任何操作。
2. UnSafe类的CAS操作
compareAndSwapObject(Object o, long offset, Object expected, Object x)
: 原子地比较并交换对象字段。compareAndSwapInt(Object o, long offset, int expected, int x)
: 原子地比较并交换整数字段。compareAndSwapLong(Object o, long offset, long expected, long x)
: 原子地比较并交换长整数字段。
3. CAS示例代码
展示了如何使用AtomicInteger
类来实现一个简单的CAS(Compare And Swap)操作。AtomicInteger
类是Java并发包java.util.concurrent.atomic
中的一部分,它提供了原子性操作的方法,这些方法底层使用了CAS指令。
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private AtomicInteger atomicInt = new AtomicInteger(0);
public void safeUpdate(int newValue) {
// 无限循环直到更新成功
while (true) {
// 获取当前值
int current = atomicInt.get();
// 尝试更新值,如果当前值未被其他线程改变,则更新成功
if (atomicInt.compareAndSet(current, newValue)) {
System.out.println("Value updated from " + current + " to " + newValue);
break;
}
// 如果更新失败,则继续循环尝试
System.out.println("Update failed, trying again...");
}
}
public static void main(String[] args) {
CASExample casExample = new CASExample();
// 启动一个线程来尝试更新值
new Thread(() -> {
casExample.safeUpdate(5);
}).start();
// 启动另一个线程来尝试更新值
new Thread(() -> {
casExample.safeUpdate(10);
}).start();
}
}
4. CAS手写自旋锁
使用Java语言实现的简单自旋锁的示例。这个示例利用了AtomicReference
类来模拟CAS操作,该类提供了原子性更新引用类型的方法。
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>(null);
public void lock() {
Thread currentThread = Thread.currentThread();
// 如果锁未被占用,则尝试获取锁
while (!owner.compareAndSet(null, currentThread)) {
// 自旋等待锁释放
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
// 只有锁的持有者才能释放锁
owner.compareAndSet(currentThread, null);
}
// 示例使用
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
// 定义一个使用自旋锁的线程
Runnable task = () -> {
spinLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
// 模拟业务逻辑处理
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
};
// 创建并启动多个线程
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
}
}
5. CAS中的ABA问题编码
ABA问题发生在多线程环境中,当以下情况发生时:
- 线程A读取了一个值(假设为A)。
- 线程A因为某些原因被挂起或阻塞,此时另一个线程B执行以下操作:
- 将值从A改为B。
- 完成一些工作后,又将值从B改回A。
- 线程A恢复执行,它再次检查这个值,发现值仍然是A,因此认为这个值没有被其他线程改变过,并基于这个假设继续执行。、
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
// 使用AtomicInteger演示ABA问题
private static AtomicInteger atomicInt = new AtomicInteger(100);
// 使用AtomicStampedReference来避免ABA问题
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0);
public static void main(String[] args) throws InterruptedException {
// 创建一个线程来演示ABA问题
Thread threadA = new Thread(() -> {
// 模拟ABA问题
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
});
// 创建一个线程来尝试在ABA问题发生后更新值
Thread threadB = new Thread(() -> {
boolean result = atomicInt.compareAndSet(100, 101);
System.out.println("AtomicInteger: " + (result ? "Update successful" : "Update failed"));
});
// 启动线程A和B
threadA.start();
threadA.join();
threadB.start();
threadB.join();
// 使用AtomicStampedReference避免ABA问题
Thread threadC = new Thread(() -> {
// 获取初始的标记
int stamp = atomicStampedRef.getStamp();
// 模拟ABA问题
atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
});
// 创建一个线程来尝试在ABA问题发生后更新值
Thread threadD = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
boolean result = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println("AtomicStampedReference: " + (result ? "Update successful" : "Update failed"));
});
// 启动线程C和D
threadC.start();
threadC.join();
threadD.start();
threadD.join();
}
}
6. CAS两大缺点
CAS(Compare And Swap)操作虽然是一种高效的同步机制,但也存在一些缺点:
-
ABA问题:
- 问题描述:CAS操作需要检查一个变量的值是否仍然保持期望的值(A),如果是,则将其更新为新值(B)。然而,如果变量的值先是A,然后被另一个线程改变为B,然后又被改回A,这时CAS操作会认为变量没有被改变过,从而成功更新为新值B。这在某些情况下可能会导致错误的行为,因为变量的状态可能已经发生了重要的变化。
- 解决方案:为了解决ABA问题,可以使用带有标记的版本号,例如
AtomicStampedReference
类,它不仅比较当前值,还比较版本号。
-
自旋开销:
- 问题描述:在竞争激烈的环境中,CAS操作可能需要多次尝试才能成功。每次尝试都涉及一个自旋循环,这会消耗CPU资源。如果许多线程都在尝试对同一个变量进行CAS操作,那么这些线程将会持续消耗CPU资源,即使它们实际上并没有做任何有用的计算工作。
- 解决方案:为了减少自旋的开销,可以采用以下策略:
- 退避策略:在失败后,线程可以暂时休眠一段时间,然后再重试。
- 线程挂起:在确定无法立即成功执行CAS操作时,线程可以选择挂起自己,而不是持续自旋。
- 使用其他同步机制:在某些情况下,可能更适合使用传统的锁或其他同步机制,以避免CAS操作带来的开销。