在Java并发编程领域,JUC(java.util.concurrent)包是处理多线程协作的核心工具集,其中CAS无锁算法和Atomic原子类的组合,为开发者提供了不需要使用重量级锁就能实现线程安全操作的能力,在高并发场景下能显著提升程序性能。
CAS无锁算法基础
CAS全称为Compare And Swap,即比较并交换,是一种乐观锁的实现思路,核心逻辑是假设多线程操作共享变量时不会产生冲突,只有在更新数据的时候才会检查当前值是否被其他线程修改过。
CAS的执行流程
CAS操作包含三个核心参数:
- V:需要更新的共享变量内存地址
- E:期望的当前变量值
- N:准备更新的新值
执行时,会先比较V地址对应的值是否等于E,如果相等,就把V地址的值更新为N,操作成功;如果不相等,说明有其他线程修改过该变量,当前操作失败,通常会进行重试或者放弃更新。
CAS的优缺点
优点在于不需要阻塞线程,避免了线程上下文切换的开销,在高并发、读多写少的场景下性能表现优异。缺点主要有三个:一是存在ABA问题,即变量从A被改成B又改回A,CAS检查时会认为没有变化;二是循环时间长开销大,如果一直更新失败会不断重试;三是只能保证单个变量的原子操作,多个变量需要配合其他机制实现原子性。
Atomic原子类概述
Atomic原子类是JUC包下基于CAS机制实现的线程安全类,所有操作都能保证原子性,不需要开发者手动加锁。根据操作的数据类型,Atomic原子类可以分为以下几类:
| 分类 | 典型类 | 适用场景 |
|---|---|---|
| 基本类型原子类 | AtomicInteger, AtomicLong, AtomicBoolean | 对int、long、boolean类型变量做原子操作 |
| 数组类型原子类 | AtomicIntegerArray, AtomicLongArray | 对数组中的元素做原子操作 |
| 引用类型原子类 | AtomicReference, AtomicStampedReference | 对对象引用做原子操作,后者可解决ABA问题 |
| 字段更新原子类 | AtomicIntegerFieldUpdater, AtomicLongFieldUpdater | 对对象的指定字段做原子操作,字段需要是volatile修饰的 |
Atomic原子类底层对CAS的应用
Atomic原子类的核心方法都依赖Unsafe类的CAS操作实现,以AtomicInteger的incrementAndGet方法为例,其底层逻辑就是不断尝试用CAS更新当前值,直到更新成功。
以下是模拟AtomicInteger自增逻辑的简化代码示例:
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class SimpleAtomicInteger {
private volatile int value;
private static final Unsafe unsafe;
private static final long valueOffset;
static {
try {
// 获取Unsafe实例
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
unsafe = (Unsafe) unsafeField.get(null);
// 获取value字段的内存偏移量
valueOffset = unsafe.objectFieldOffset(SimpleAtomicInteger.class.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
public SimpleAtomicInteger(int initialValue) {
this.value = initialValue;
}
// 自增并返回自增后的值,模拟AtomicInteger的incrementAndGet方法
public final int incrementAndGet() {
int current;
do {
// 获取当前value值作为期望值
current = unsafe.getIntVolatile(this, valueOffset);
// 期望值加1作为新值,尝试CAS更新
} while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
// 更新成功,返回新值
return current + 1;
}
public int get() {
return value;
}
}
上述代码中,compareAndSwapInt就是Unsafe类提供的CAS本地方法,如果当前对象的valueOffset偏移量对应的值等于current,就更新为current + 1,否则循环重试,直到更新成功,这就是Atomic原子类无锁实现原子操作的核心逻辑。
典型应用场景示例
CAS和Atomic原子类非常适合计数器、状态标记、简单版本号控制等场景,以下是两个常见应用示例:
线程安全计数器
使用AtomicInteger实现高并发场景下的计数,不需要加锁就能保证计数准确:
import java.util.concurrent.atomic.AtomicInteger;
public class CounterDemo {
private static final AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// 创建10个线程,每个线程计数1000次
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.incrementAndGet();
}
});
threads[i].start();
}
// 等待所有线程执行完成
for (Thread thread : threads) {
thread.join();
}
// 输出最终计数结果,应该是10000
System.out.println("最终计数结果:" + counter.get());
}
}
解决ABA问题的版本号控制
如果需要避免ABA问题,可以使用AtomicStampedReference,它在CAS的基础上增加了版本号,每次更新都会递增版本号,即使值被改回原值,版本号也不同,CAS会识别到变化:
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
public static void main(String[] args) {
// 初始值100,初始版本号0
AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(100, 0);
Thread t1 = new Thread(() -> {
int stamp = reference.getStamp();
System.out.println("线程1初始版本号:" + stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试将100改成101,再改回100,版本号会递增
reference.compareAndSet(100, 101, stamp, stamp + 1);
reference.compareAndSet(101, 100, reference.getStamp(), reference.getStamp() + 1);
});
Thread t2 = new Thread(() -> {
int stamp = reference.getStamp();
System.out.println("线程2初始版本号:" + stamp);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 线程2用初始版本号尝试更新,此时版本号已经被线程1修改,更新失败
boolean success = reference.compareAndSet(100, 200, stamp, stamp + 1);
System.out.println("线程2更新是否成功:" + success);
System.out.println("当前值:" + reference.getReference() + ",当前版本号:" + reference.getStamp());
});
t1.start();
t2.start();
}
}
上述代码中,线程1修改了两次值,版本号从0变成了2,线程2用初始版本号0尝试更新,会失败,这样就避免了ABA问题带来的逻辑错误。
使用注意事项
在实际使用CAS和Atomic原子类时,需要注意几个问题:一是如果竞争激烈,CAS重试次数过多会导致CPU开销增大,此时可以考虑使用锁或者其他并发方案;二是ABA问题是否会影响业务逻辑,如果会影响,需要选择带版本号的原子类;三是Atomic原子类只能保证单个操作的原子性,多个原子操作的组合需要额外保证原子性,比如先判断再更新多个变量,不能依赖单个Atomic类的操作。