文章目录
- 1、 什么是CAS
- 2、CAS的原理
- 3、CAS的应用场景
- 4、Java中的CAS实现
- 5、使用AtomicInteger实现线程安全的计数器
- 6、CAS实现原子操作的三大问题
- 6.1、ABA问题
- 6.2、循环时间长
- 6.3、只能保证一个共享变量的原子性
- 7、总结
1、 什么是CAS
CAS(Compare and Swap)是一种并发编程中的技术,用于实现多线程之间的原子操作。它允许你比较一个内存位置的值和一个预期的值,如果相等,则将新值设置到该内存位置,从而实现原子性的修改操作。CAS是乐观锁,线程执行的时候不会加锁,假设没有冲突去完成某项操作,如果因为冲突失败了就重试,最后直到成功为止。
2、CAS的原理
CAS操作主要包括两个步骤:
比较:首先,将内存位置的当前值与预期值进行比较。
交换:如果当前值与预期值相等,就将新值设置到内存位置。
前面提到,CAS是一种原子操作。那么Java是怎样来使用CAS的呢?我们知道,在Java中,如果一个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM使用c或者c++去实现。
在Java中,有一个Unsafe类,它在sun.misc包中。它里面是一些native方法,其中就有几个关于CAS的:
boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset,int expected,int x);
boolean compareAndSwapLong(Object o, long offset,long expected,long x);
当然,他们都是public native的。
Unsafe中对CAS的实现是C++写的,它的具体实现和操作系统、CPU都有关系。
Linux的X86下主要是通过cmpxchgl这个指令在CPU级完成CAS操作的,但在多处理器情况下必须使用lock指令加锁来完成。当然不同的操作系统和处理器的实现会有所不同,大家可以自行了解。
当然,Unsafe类里面还有其它方法用于不同的用途。比如支持线程挂起和恢复的park和unpark, LockSupport类底层就是调用了这两个方法。还有支持反射操作的allocateInstance()方法。
3、CAS的应用场景
线程安全问题:CAS可以用于解决多线程环境下的数据竞争问题,如计数器、标志位等。
性能优化:CAS避免了使用锁的开销,适用于一些高并发、频繁修改的场景。
4、Java中的CAS实现
Java中提供了java.util.concurrent.atomic包,包含了一系列基于CAS的原子类,如AtomicInteger、AtomicLong等。
5、使用AtomicInteger实现线程安全的计数器
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger value = new AtomicInteger(0);
public void increment() {
value.incrementAndGet();
}
public int getValue() {
return value.get();
}
}
为什么AtomicInteger就是原子性的操作,这里我们以AtomicInteger类的getAndAdd(int delta)方法为例,来看看Java是如何实现原子操作的。
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
这里的U其实就是Unsafe对象;
6、CAS实现原子操作的三大问题
尽管CAS在很多情况下是非常有用的,但它也存在三大经典问题:ABA问题、循环时间长以及只能保证一个共享变量的原子性。
6.1、ABA问题
ABA问题指的是,在CAS操作期间,某个值从A变为B,然后再变回A,而CAS操作的结果可能会错误地判定为成功。这是因为CAS只比较当前值和预期值,不考虑中间是否发生过其他修改。解决ABA问题的方法是引入版本号,即将共享变量的值和版本号一起进行比较和交换。
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABAExample {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(1, 0);
int initialStamp = atomicStampedRef.getStamp();
int initialValue = atomicStampedRef.getReference();
// 线程A执行操作,此时线程B修改了值,又恢复原值
atomicStampedRef.compareAndSet(initialValue, 2, initialStamp, initialStamp + 1);
// 线程C试图修改值,但由于版本号不匹配,CAS失败
boolean success = atomicStampedRef.compareAndSet(initialValue, 3, initialStamp, initialStamp + 1);
}
}
6.2、循环时间长
CAS操作是基于循环实现的,即在操作失败时会重试,直至操作成功为止。但当竞争激烈时,可能会导致循环时间较长,浪费CPU资源。解决这个问题的方法是在重试时加入一定的时间限制,避免无限循环。
import java.util.concurrent.atomic.AtomicInteger;
public class LongLoopExample {
public static void main(String[] args) {
AtomicInteger counter = new AtomicInteger(0);
int expectedValue = 0;
int newValue = 1;
int maxRetries = 10;
int retries = 0;
while (retries < maxRetries) {
if (counter.compareAndSet(expectedValue, newValue)) {
// CAS成功,退出循环
break;
}
retries++;
}
}
}
6.3、只能保证一个共享变量的原子性
CAS操作只能保证单个共享变量的原子性,不能解决多个共享变量之间的原子操作问题。这意味着如果需要同时修改多个共享变量,CAS可能无法保证这些操作的原子性。
import java.util.concurrent.atomic.AtomicInteger;
public class MultipleVariablesExample {
public static void main(String[] args) {
AtomicInteger value1 = new AtomicInteger(0);
AtomicInteger value2 = new AtomicInteger(0);
value1.incrementAndGet(); // CAS保证了value1的原子操作
value2.incrementAndGet(); // 但不能保证value1和value2操作的原子性
}
}
7、总结
虽然CAS存在这三大问题,但在很多情况下仍然是非常有用的,并且在并发编程中发挥着重要的作用。为了解决这些问题,有时需要结合其他技术,如引入版本号来解决ABA问题,或者使用锁来处理多个共享变量之间的复杂原子操作。