Java CAS 与 AQS
1. CAS
1.1 CAS 原理
CAS(Compare-And-Swap)即比较并交换,是一种无锁算法,用于实现多线程环境下的原子操作。它是一种乐观锁的实现方式,其核心思想是:假设数据在没有被其他线程修改的情况下进行操作,如果发现数据被修改了,则重新尝试操作,直到成功为止。
CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置 V 的值等于预期原值 A 时,才将该位置的值更新为新值 B;否则,不做任何操作。整个操作是原子性的,由硬件层面来保证。
在 Java 中,java.util.concurrent.atomic
包下的原子类就是基于 CAS 实现的,例如 AtomicInteger
、AtomicLong
等。
1.2 使用示例
使用 AtomicInteger
进行自增操作
import java.util.concurrent.atomic.AtomicInteger;
public class CASAtomicIntegerExample {
public static void main(String[] args) {
// 创建一个 AtomicInteger 对象,初始值为 0
AtomicInteger atomicInteger = new AtomicInteger(0);
// 创建两个线程,每个线程对 atomicInteger 进行 1000 次自增操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicInteger.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicInteger.incrementAndGet();
}
});
// 启动线程
t1.start();
t2.start();
try {
// 等待两个线程执行完毕
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终结果
System.out.println("Final value: " + atomicInteger.get());
}
}
解释:
-
AtomicInteger
是 Java 提供的一个原子整数类,它内部使用 CAS 机制来保证对整数的操作是原子的。 -
incrementAndGet()
方法会原子地将AtomicInteger
的值加 1,并返回加 1 后的值。 -
在多线程环境下,多个线程可以同时调用
incrementAndGet()
方法,而不会出现数据不一致的问题。
1.3 CAS 的局限性
-
ABA 问题:CAS 操作只关注值是否相等,而不关心值的变化过程。如果一个值从 A 变为 B,再从 B 变回 A,CAS 操作会认为值没有发生变化,从而继续进行更新操作。可以使用
AtomicStampedReference
或AtomicMarkableReference
来解决 ABA 问题。 -
循环时间长开销大:如果 CAS 操作长时间不成功,会导致线程不断地进行自旋,从而消耗大量的 CPU 资源。
-
只能保证一个共享变量的原子操作:CAS 只能保证对一个共享变量的原子操作,如果需要对多个共享变量进行原子操作,需要使用锁或其他同步机制。
2. AQS
AQS
(AbstractQueuedSynchronizer)是 Java 并发包(java.util.concurrent
)中用于构建锁和同步器的基础框架。许多 Java 并发工具类,如 ReentrantLock
、CountDownLatch
、Semaphore
等都是基于 AQS 实现的。
1.1 核心组成部分
-
同步状态(
state
):一个int
类型的变量,用于表示同步状态。不同的同步器可以根据这个状态来实现不同的同步逻辑。例如,在ReentrantLock
中,state
为 0 表示锁未被持有,大于 0 表示锁已经被持有,并且数值表示重入的次数。 -
队列(CLH 队列的变种):一个双向链表,用于管理那些获取同步状态失败的线程。当一个线程尝试获取同步状态失败时,会被封装成一个节点加入到队列的尾部,进入等待状态。当持有同步状态的线程释放状态后,会从队列的头部唤醒一个等待线程。
-
CAS(Compare-And-Swap)操作:用于保证对
state
变量的原子性修改。CAS 操作是一种无锁算法,通过比较内存中的值和预期值是否相等,如果相等则将内存中的值更新为新值。
1.2 工作流程
-
获取同步状态:线程尝试通过
tryAcquire
方法获取同步状态,如果获取成功,则继续执行;如果获取失败,则将该线程封装成节点加入到等待队列中,并进入阻塞状态。 -
释放同步状态:线程通过
tryRelease
方法释放同步状态,释放成功后,会从队列的头部唤醒一个等待线程。 -
队列管理:当有新的线程获取同步状态失败时,会将其封装成节点加入到队列的尾部;当持有同步状态的线程释放状态后,会从队列的头部唤醒一个等待线程。
1.3 自定义同步器示例
通过一个自定义同步器来演示 AQS
的使用,实现一个简单的独占锁。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
// 自定义同步器
class MySync extends AbstractQueuedSynchronizer {
// 尝试获取锁
@Override
protected boolean tryAcquire(int arg) {
// 使用 CAS 操作尝试将状态从 0 变为 1
if (compareAndSetState(0, 1)) {
// 设置当前线程为独占线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 尝试释放锁
@Override
protected boolean tryRelease(int arg) {
// 检查当前线程是否为独占线程
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
// 将状态设置为 0
setState(0);
// 清除独占线程
setExclusiveOwnerThread(null);
return true;
}
// 判断是否处于锁定状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
// 自定义独占锁
class MyLock {
private final MySync sync = new MySync();
// 加锁
public void lock() {
sync.acquire(1);
}
// 解锁
public void unlock() {
sync.release(1);
}
// 判断是否锁定
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
// 测试类
public class AQSExample {
public static void main(String[] args) {
MyLock lock = new MyLock();
// 线程 1 尝试获取锁
Thread thread1 = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread 1 acquired the lock.");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("Thread 1 released the lock.");
}
});
// 线程 2 尝试获取锁
Thread thread2 = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread 2 acquired the lock.");
} finally {
lock.unlock();
System.out.println("Thread 2 released the lock.");
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
解释
-
MySync 类:继承自
AbstractQueuedSynchronizer
,并重写了tryAcquire
、tryRelease
和isHeldExclusively
方法。-
tryAcquire
:尝试获取锁,使用CAS
操作将状态从 0 变为 1,如果成功则将当前线程设置为独占线程。 -
tryRelease
:尝试释放锁,检查当前线程是否为独占线程,如果是则将状态设置为 0,并清除独占线程。 -
isHeldExclusively
:判断当前是否处于锁定状态。
-
-
MyLock 类:封装了
MySync
对象,提供了lock
、unlock
和isLocked
方法。-
lock
:调用sync.acquire(1)
方法获取锁。 -
unlock
:调用sync.release(1)
方法释放锁。 -
isLocked
:调用sync.isHeldExclusively()
方法判断是否锁定。
-
-
AQSExample 类:创建了两个线程,分别尝试获取和释放锁,演示了自定义锁的使用。
注意事项
-
在使用
AQS
时,需要根据具体需求重写tryAcquire
、tryRelease
等方法,确保同步状态的正确管理。 -
AQS
提供了公平锁和非公平锁的实现,需要根据具体场景选择合适的锁策略。