Java AQS框架:getState()和setState()实现同步状态的原子操作
大家好,今天我们要深入探讨Java并发包java.util.concurrent.locks中的一个核心框架:AbstractQueuedSynchronizer (AQS),以及它如何利用getState()和setState()方法实现同步状态的原子操作。 AQS是构建锁和其他同步组件的基础,理解它的工作原理对于编写高效、可靠的并发程序至关重要。
1. AQS 框架概述
AQS(AbstractQueuedSynchronizer),即抽象队列同步器,是一个用来构建锁和同步器的框架。它内部维护了一个同步状态(state)和一个FIFO队列。 AQS的设计思想是将同步状态的管理和线程的阻塞/唤醒机制解耦。 子类只需要实现对同步状态的控制,而无需关心线程的排队和唤醒细节,这些由AQS框架负责处理。
AQS基于模板方法模式,提供了一系列的protected方法供子类实现,用来控制同步状态。 其中,getState()和setState()是两个最基础的方法,用于获取和设置同步状态。
2. getState()和setState():同步状态的基石
getState()和setState()方法是AQS提供的两个原子操作,用于读取和设置同步状态state的值。
getState(): 以int类型返回当前同步状态的值。protected final int getState() { return state; }setState(int newState): 原子地设置同步状态的值为newState。protected final void setState(int newState) { state = newState; }
这两个方法本身很简单,但它们是构建更高级同步机制的基础。 state变量是volatile类型的,保证了可见性。 然而,单纯的读写state变量并不能保证原子性,例如,多个线程同时尝试增加state的值,可能会发生竞态条件。 这就是为什么AQS还需要借助CAS(Compare and Swap)操作。
3. CAS 操作:原子更新的关键
CAS (Compare and Swap) 是一种无锁算法,用于原子地更新一个变量的值。 它包含三个操作数:
- V (Value): 要更新的变量的值。
- E (Expected): 期望的值。
- N (New): 新的值。
CAS操作会比较V的值是否等于E,如果相等,则将V的值更新为N,否则,不做任何操作。 整个过程是原子性的。
AQS使用compareAndSetState(int expect, int update)方法来实现CAS操作。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics explanation.
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
这个方法利用了sun.misc.Unsafe类提供的底层原子操作。 unsafe.compareAndSwapInt()是一个native方法,由JVM保证其原子性。
4. 利用getState()、setState()和CAS实现同步
AQS并没有直接提供incrementState()或decrementState()这样的方法,而是鼓励子类使用getState()、setState()和compareAndSetState()组合来实现更复杂的同步逻辑。
例如,要实现一个简单的互斥锁,可以这样:
- 初始状态
state = 0(表示锁未被占用)。 acquire()操作:尝试将state从 0 更新为 1。 如果成功,则获取锁;否则,进入等待队列。release()操作:将state从 1 更新为 0,并唤醒等待队列中的一个线程。
下面是一个简单的互斥锁的实现示例:
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;
public class SimpleMutex implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
// 尝试获取锁
@Override
protected boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 尝试释放锁
@Override
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 是否独占模式
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long timeout, java.util.concurrent.TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
@Override
public java.util.concurrent.locks.Condition newCondition() {
throw new UnsupportedOperationException("Condition not supported.");
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
在这个例子中,tryAcquire()方法使用compareAndSetState(0, 1)来尝试原子地将state从0更新为1。 如果成功,表示获取锁成功,设置当前线程为独占所有者线程。 tryRelease()方法将state设置为0,释放锁。
5. AQS 状态表示:不仅仅是 0 和 1
虽然简单的互斥锁使用 0 和 1 来表示锁的状态,但 AQS 的 state 变量是一个 int 类型,可以表示更复杂的状态。 例如,ReentrantLock使用state来表示锁被重入的次数。 当一个线程第一次获取锁时,state被设置为1。 如果同一个线程再次获取锁,state会递增。 释放锁时,state递减,直到变为0时才真正释放锁。
这种状态表示方式允许 AQS 支持各种复杂的同步语义。
6. AQS 的同步模式:独占模式和共享模式
AQS支持两种同步模式:
- 独占模式 (Exclusive Mode): 一次只允许一个线程获取同步状态。 比如
ReentrantLock。 - 共享模式 (Shared Mode): 允许多个线程同时获取同步状态。 比如
CountDownLatch和Semaphore。
子类需要根据自己的同步语义选择合适的同步模式。
- 独占模式: 子类需要实现
tryAcquire(int arg)和tryRelease(int arg)方法。 - 共享模式: 子类需要实现
tryAcquireShared(int arg)和tryReleaseShared(int arg)方法。
tryAcquireShared(int arg) 方法返回一个 int 值:
- 正数:表示获取成功,并且剩余可用的资源数。
- 零:表示获取成功,但是没有剩余资源。
- 负数:表示获取失败。
tryReleaseShared(int arg) 方法返回一个 boolean 值:
true:表示释放资源后,可以唤醒等待队列中的线程。false:表示释放资源后,不需要唤醒等待队列中的线程。
7. AQS 中的模板方法
AQS提供了一系列的模板方法,供子类调用来实现同步逻辑。 常见的模板方法包括:
| 方法 | 描述 |
|---|---|
tryAcquire(int arg) |
尝试以独占模式获取同步状态。 如果获取成功,则返回 true,否则返回 false。 |
tryRelease(int arg) |
尝试以独占模式释放同步状态。 如果释放成功,则返回 true,否则返回 false。 |
tryAcquireShared(int arg) |
尝试以共享模式获取同步状态。 返回值:正数表示获取成功且有剩余资源,零表示获取成功但无剩余资源,负数表示获取失败。 |
tryReleaseShared(int arg) |
尝试以共享模式释放同步状态。 返回值:true 表示释放后可以唤醒等待线程,false 表示不需要唤醒。 |
isHeldExclusively() |
如果当前线程以独占模式持有同步状态,则返回 true。 |
acquire(int arg) |
以独占模式获取同步状态,如果获取失败,则进入等待队列,直到获取成功。 |
acquireInterruptibly(int arg) |
以独占模式获取同步状态,如果获取失败,则进入等待队列,直到获取成功或者被中断。 |
tryAcquireNanos(int arg, long nanosTimeout) |
在指定的时间内,尝试以独占模式获取同步状态。 |
release(int arg) |
以独占模式释放同步状态,并唤醒等待队列中的一个线程。 |
acquireShared(int arg) |
以共享模式获取同步状态,如果获取失败,则进入等待队列,直到获取成功。 |
acquireSharedInterruptibly(int arg) |
以共享模式获取同步状态,如果获取失败,则进入等待队列,直到获取成功或者被中断。 |
tryAcquireSharedNanos(int arg, long nanosTimeout) |
在指定的时间内,尝试以共享模式获取同步状态。 |
releaseShared(int arg) |
以共享模式释放同步状态,并唤醒等待队列中的一个或多个线程。 |
8. AQS 在 ReentrantLock 和 CountDownLatch 中的应用
为了更好地理解AQS,我们来看一下ReentrantLock和CountDownLatch是如何利用AQS来实现同步的。
-
ReentrantLock: 实现了可重入的互斥锁。 它使用state来表示锁被重入的次数。tryAcquire(int arg):- 如果
state == 0,表示锁未被占用,尝试使用CAS将state设置为1,获取锁。 - 如果
state > 0且当前线程是锁的持有者,则将state递增,重入锁。 - 否则,获取锁失败。
- 如果
tryRelease(int arg):- 将
state递减。 - 如果
state == 0,表示锁已经完全释放,设置锁的持有者为null,并返回true。 - 否则,返回
false。
- 将
-
CountDownLatch: 允许一个或多个线程等待其他线程完成操作。 它使用state来表示需要等待的事件数量。tryAcquireShared(int arg):- 如果
state == 0,表示所有事件已经发生,允许所有等待的线程通过。 - 否则,获取失败。
- 如果
tryReleaseShared(int arg):- 将
state递减。 - 如果
state == 0,表示所有事件已经发生,唤醒所有等待的线程。
- 将
9. AQS 的优势和局限性
优势:
- 代码复用: AQS 提供了通用的同步机制,子类只需要关注同步状态的控制,无需关心线程的排队和唤醒细节。
- 灵活性: AQS 可以支持各种同步语义,包括互斥锁、读写锁、信号量等等。
- 性能: AQS 使用了 CAS 操作和 FIFO 队列,可以提供较高的并发性能。
局限性:
- 复杂性: AQS 的实现比较复杂,需要深入理解其工作原理才能正确使用。
- 需要自己实现同步逻辑: AQS只提供框架,具体的同步逻辑(比如如何判断是否可以获取锁,如何释放锁)需要子类自己实现。
10. 注意事项:避免常见错误
在使用 AQS 时,需要注意以下几点:
- 正确理解同步状态的含义: 不同的同步器有不同的状态含义,例如互斥锁是0/1,可重入锁是重入次数,CountDownLatch是剩余计数。
- 正确实现
tryAcquire()和tryRelease()方法: 这两个方法是 AQS 的核心,必须正确实现才能保证同步的正确性。 - 避免死锁: 在使用多个锁时,需要注意锁的获取顺序,避免死锁。
- 正确处理中断: 在获取锁时,应该能够响应中断,避免线程永久阻塞。
acquireInterruptibly()方法可以用来响应中断。
11. 用表格归纳CAS操作
| 操作 | 描述 |
|---|---|
| 比较 | 将内存位置的值与给定的预期值进行比较 |
| 交换 | 如果内存位置的值与预期值匹配,则使用给定的新值替换该值 |
| 原子性 | 整个比较和交换操作以原子方式执行,这意味着它不能被其他线程中断 |
| 成功/失败 | 如果交换成功,则CAS操作返回true;否则,返回false。失败表示另一个线程在此操作之前修改了该值 |
12. 用表格对比独占模式和共享模式
| 特性 | 独占模式 | 共享模式 |
|---|---|---|
| 线程数量 | 仅允许一个线程访问资源 | 允许多个线程同时访问资源 |
| 适用场景 | 保护临界区,防止数据竞争 | 允许并发读取,提高吞吐量 |
| 典型应用 | ReentrantLock | CountDownLatch, Semaphore |
| AQS方法 | tryAcquire(), tryRelease() | tryAcquireShared(), tryReleaseShared() |
13. AQS 的价值与应用
AQS 的价值在于它提供了一个高度灵活和可扩展的框架,极大地简化了并发组件的开发。 Java 并发包中的许多同步工具类,如 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 和 CyclicBarrier 等,都是基于 AQS 构建的。
理解 AQS 的原理,能够帮助我们更好地理解和使用这些并发工具类,并能够根据实际需求,自定义同步器,解决复杂的并发问题。
14. 理解 AQS 的 state 和 CAS 操作是关键
掌握 AQS 的核心在于理解其内部的 state 状态以及如何使用 CAS 操作来原子地更新这个状态。 getState() 和 setState() 方法是访问和修改状态的基础,而 compareAndSetState() 方法则保证了状态更新的原子性,避免了竞态条件。 通过灵活地组合这些方法,我们可以构建各种复杂的同步器,满足不同的并发需求。
15. AQS的强大之处
AQS之所以强大,在于它将复杂的同步逻辑抽象成一个可复用的框架。开发者只需要关注同步状态的定义和状态转换的规则,而无需关心线程的排队、阻塞和唤醒等底层细节。这种分层设计极大地提高了并发编程的效率和可维护性。