前言
本文将讲述CAS的实现原理;ABA问题是什么,如何解决ABA;## Unsafe类是啥。
CAS
在Java中,JUC(Java Util Concurrent)是Java并发编程中提供的一个工具包,其中包含了一些用于处理并发编程的工具和类库。CAS(Compare and Swap)是JUC中的一个重要概念和机制之一。
CAS中有重要的三个操作数:一个内存位置(通常是共享变量)、预期值和新值。
CAS是一种乐观锁机制,用于实现多线程环境下的原子操作。它通过比较内存中的值与预期值是否相等来确定是否能够更新内存中的值。如果相等,表示预期值未被其他线程修改,CAS操作将原子地将新值写入内存。如果不相等,表示其他线程已经修改了预期值,CAS操作会失败,不会进行写入操作。
在JUC中,CAS主要通过java.util.concurrent.atomic包中的原子类来实现。这些原子类提供了一组基于CAS操作的原子性方法,如compareAndSet、getAndSet等。常见的原子类包括AtomicInteger、AtomicLong、AtomicBoolean等,它们可以用来实现对基本类型的原子操作。
CAS操作的优点是不需要使用锁(互斥锁)来保护共享资源,避免了线程阻塞和上下文切换的开销,可以提高并发性能。然而,CAS操作也存在一些问题,例如ABA问题和循环时间开销等,需要在使用时进行考虑。
CAS如何保证原子性
CAS底层是一条CPU的原子指令cmpxchg,该指令接受三个操作数:一个内存位置(通常是共享变量)、预期值和新值。CAS操作的执行过程如下:
mpxchg`指令的步骤可以概括为以下几个部分:
- 读取内存值:首先,指令会读取内存中指定地址的值,并将其与寄存器中的值进行比较。
- 比较操作:接下来,指令会比较内存值与寄存器值的大小或相等性。这个比较操作可以使用处理器的比较单元执行。
- 交换操作:如果内存值与寄存器值相等,那么内存值将保持不变,而寄存器的值也不会发生变化。如果内存值与寄存器值不相等,那么内存值将被用寄存器的值替换,并更新内存中的值。
- 原子性保证:
cmpxchg指令的关键特性是原子性,即在执行比较和交换的过程中保证不被中断或干扰。这意味着其他并行执行的指令不会在这个指令执行过程中修改内存中的值,确保了操作的原子性和一致性。
执行cmpxchg指令时会判断当前系统是否有多核处理器,如果为多核,则会加锁,否则不加锁,也就是说CAS的原子性其实是CPU独占实现的。
需要注意的是,尽管CAS操作本身是原子的,但它无法解决在操作过程中发生的数据变化,例如ABA问题。为了解决这类问题,可以在CAS操作中添加额外的标记位或版本号,以便检测数据变化。
Unsafe类
是CAS的核心类,它提供了直接操作内存和执行低级别、不安全操作的能力。由于Unsafe类中的方法可以绕过Java语言的安全检查和限制,因此它被称为"不安全类"。
自旋锁
用于在多线程编程中实现对共享资源的互斥访问。它与传统的互斥锁(如悲观锁)相比,采用了一种不阻塞线程的策略,通过循环重试的方式等待锁的释放,从而避免线程切换和阻塞导致的性能开销。
自旋锁的基本思想是当线程请求获取锁时,如果锁已经被其他线程持有,则当前线程不会放弃CPU的执行权,而是反复检查锁的状态,直到锁被释放。当锁被释放后,请求锁的线程立即获得锁并进入临界区,完成操作后释放锁,以便其他线程获取锁并执行。
自旋锁适用于共享资源的竞争情况短暂且线程持有锁的时间较短的情况,因为在循环重试的过程中,会导致CPU会空转。如果持有锁的时间较长,或者资源竞争激烈,自旋的线程会占用大量的CPU资源,导致性能下降。
案例:
import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
// 自旋重试
while (!locked.compareAndSet(false, true)) {
}
}
public void unlock() {
locked.set(false);
}
}
public class SpinLockExample {
private static SpinLock spinLock = new SpinLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
spinLock.lock();
Thread.sleep(5000);
spinLock.unlock();
}
});
// 睡1秒,让t1线程先执行
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
spinLock.lock();
spinLock.unlock();
}
});
t1.start();
t2.start();
}
}
t1线程在执行拿到锁后,会睡5秒,一秒过后开启t2线程,t2在执行时,因为t1要执行5秒钟,所以t2线程就会自旋重试4秒
在unsafe类中的体现
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
CAS的缺点
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
- 循环时间长开销很大。
- 只能保证一个共享变量的原子操作。
- ABA问题。
ABA问题
ABA问题是在并发编程中出现的一种特殊情况,它指的是在CAS(Compare and Swap)操作期间,被修改的值先经历了一次修改为A,然后又经历了一次修改回原来的值B,最后再次修改为A的情况。
例如,假设有两个线程T1和T2同时对某个共享变量进行操作:
- 初始状态下,共享变量的值为A。
- 线程T1读取共享变量的值为A。
- 线程T2读取共享变量的值为A并先进行CAS操作。
- 线程T2读取内存位置的值跟旧预期值比较,相等,将共享变量的值修改为B。
- 线程T2又将共享变量的值修改回A,成功CAS操作结束。
- 线程T1此时才能进行CAS操作。
- 线程T1拿到旧预期值然后获取内存位置的值比较,发生还是相等,CAS操作成功。
- 可此时共享变量的值是经过了多次修改的,只不过最终值没变
在这个过程中,虽然最终共享变量的值与开始时的值相同,但是实际上在中间经历了一次从A到B再到A的变化。由于CAS操作只比较值是否相等,因此在这种情况下,CAS操作会误认为共享变量的值没有发生变化,从而可能导致程序逻辑错误。
A解决ABA问题的一种常见方法是使用版本号或标记位,可以使用AtomicStampedReference类解决,该类就是使用了类似版本号的机制。
下面是一个使用AtomicStampedReference解决ABA问题的案例:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolutionExample {
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0);
public static void main(String[] args) {
// 线程A,先将值修改为200,再修改回100
Thread threadA = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
System.out.println("Thread A: Value = " + atomicStampedRef.getReference() + ", Stamp = " + stamp);
atomicStampedRef.compareAndSet(100, 200, stamp, stamp + 1);
System.out.println("Thread A: Value after first modification = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp());
atomicStampedRef.compareAndSet(200, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
System.out.println("Thread A: Value after second modification = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp());
});
// 线程B,将值修改为300
Thread threadB = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
System.out.println("Thread B: Value = " + atomicStampedRef.getReference() + ", Stamp = " + stamp);
try {
TimeUnit.SECONDS.sleep(1); // 等待线程A执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedRef.compareAndSet(100, 300, stamp, stamp + 1);
System.out.println("Thread B: Update result = " + result + ", Value = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp());
});
threadA.start();
threadB.start();
}
}
在这个例子中,我们使用了AtomicStampedReference类来解决ABA问题。该类是AtomicReference的扩展,它在CAS操作的基础上增加了一个表示版本号的标记(stamp)。每次执行CAS操作时,除了比较引用的值外,还需要比较版本号,从而可以更准确地判断变量是否发生了意义上的变化。