面试官: 你对AQS框架了解多少?
我回答:
AQS(AbstractQueuedSynchronizer)是 Java 并发包 java.util.concurrent
中的一个抽象类,它是许多同步工具类的基础,如 ReentrantLock
, Semaphore
, CountDownLatch
等。AQS 提供了一套完整的实现锁和同步器的框架,使得开发者可以基于它构建自己的同步组件。
下面我将详细介绍 AQS 的核心概念、工作原理以及如何使用 AQS 构建自定义同步器。
AQS 的核心概念
AQS(AbstractQueuedSynchronizer)是JDK提供的一个用于实现基于FIFO(First In, First Out)等待队列的阻塞锁和同步器的基础框架。它定义了一套多线程访问共享资源的同步器框架,许多Java中的同步器都是基于AQS实现的,如ReentrantLock、Semaphore、CountDownLatch等。
-
同步状态 (
state
):- AQS 维护了一个volatile修饰的int类型变量,表示同步状态。对于独占锁,state表示锁的持有状态(0表示未持有,1表示持有);对于共享锁,state表示当前持有的读锁数量。通过CAS(Compare-And-Swap)操作来保证状态变量的原子性更新。
- 同步状态可以用来表示锁的持有情况、信号量的许可数量等。
state
的值可以通过getState()
,setState(int newState)
,compareAndSetState(int expect, int update)
方法来读取、设置或原子地更新。
-
条件队列 (
CLH queue
):- CLH (Craig-Landin-Baker) 队列是一种特殊的 FIFO 队列,用于存储等待获取同步状态的线程。
- 当线程尝试获取同步状态失败时,会被插入到条件队列中等待。
- 条件队列中的节点类型为
Node
,每个节点代表一个等待的线程。
-
独占式与共享式同步:
- 独占式同步:一次只有一个线程可以获得同步状态,如 ReentrantLock。
- 共享式同步:允许多个线程同时获得同步状态,如 Semaphore。
AQS 的工作原理
-
获取同步状态:
- 当线程调用
acquire
或tryAcquire
方法时,AQS 会尝试获取同步状态。当线程尝试获取锁时,首先会检查同步状态是否满足(对于独占锁,检查state是否为0;对于共享锁,检查state是否允许更多的线程访问)。 - 如果获取成功,则通过CAS操作尝试更新同步状态,线程可以继续执行。
- 如果获取失败,线程会被插入到条件队列中,并挂起等待。
- 当线程调用
-
释放同步状态:
- 当线程完成任务并释放同步状态时,AQS 会唤醒条件队列中的下一个节点对应的线程,使其有机会再次尝试获取同步状态。
-
线程加入等待队列和等待队列中的线程:
- 未获得锁的线程会被封装成一个Node节点,并加入到等待队列的末尾。
- 节点之间通过prev和next指针相互连接,形成一个双向链表。
- 等待的线程会被封装成
Node
类型的对象,并被插入到条件队列中。 - 当前线程会通过
LockSupport.park(this)
方法挂起自身,等待被唤醒。
-
线程阻塞与唤醒:
- 未获得锁的线程会通过Unsafe类中的park方法被阻塞。
- 当持有锁的线程释放锁时,当同步状态被释放时,AQS 会从条件队列中选择一个节点,并将其对应的线程唤醒。
- 被唤醒的线程会再次尝试获取同步状态。
AQS 的关键方法
-
tryAcquire
:- 这是一个模板方法,由子类实现,用于尝试获取同步状态。
- 如果成功,返回
true
;如果失败,返回false
。
-
tryRelease
:- 这也是一个模板方法,由子类实现,用于尝试释放同步状态。
- 如果成功,返回
true
;如果失败,返回false
。
-
acquire
:- 当
tryAcquire
失败时,调用此方法将线程插入条件队列,并可能挂起线程。 - 此方法会循环尝试获取同步状态,直到成功。
- 当
-
release
:- 当线程释放同步状态时调用此方法。
- 此方法会唤醒条件队列中的下一个节点对应的线程。
-
tryAcquireShared
:- 在共享模式下尝试获取同步状态。
-
tryReleaseShared
:- 在共享模式下尝试释放同步状态。
-
isHeldExclusively
:- 检查当前线程是否正在独占资源。
应用场景
AQS作为Java并发编程的重要基石,广泛应用于各种同步器的实现中。以下是一些基于AQS实现的同步器及其应用场景:
- ReentrantLock:一种可重入的独占锁,支持公平锁和非公平锁两种模式。
- Semaphore:一种计数信号量,用于控制对资源的访问数量,可以在独占模式和共享模式下使用。
- CountDownLatch:一种同步工具,允许一个或多个线程等待一组操作完成。
- ReadWriteLock:读写锁,允许多个线程同时读取共享资源,但写入操作是互斥的。
自定义同步器示例
下面是一个简单的自定义同步器示例,实现了一个简单的计数锁,它可以被多个线程共享:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class CountingLock extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1L;
@Override
protected boolean tryAcquire(int arg) {
// 尝试获取同步状态
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
// 释放同步状态
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() {
acquire(1);
}
public boolean tryLock() {
return tryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return acquireQueuedInterruptibly(new Node(), 1);
}
public void unlock() {
release(1);
}
public boolean isLocked() {
return getState() == 1;
}
public static void main(String[] args) {
CountingLock countingLock = new CountingLock();
// 测试锁的功能
Thread t1 = new Thread(() -> {
countingLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countingLock.unlock();
}
});
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(() -> {
countingLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countingLock.unlock();
}
});
t2.start();
}
}
在这个示例中,我们定义了一个名为 CountingLock
的类,它继承自 AbstractQueuedSynchronizer
。我们实现了 tryAcquire
和 tryRelease
方法来控制同步状态的获取和释放。此外,我们还定义了一些公共方法来方便外部调用。
总结
AQS是Java并发编程中的一个重要组件,它提供了一个构建锁和同步器的框架,通过状态变量和等待队列的管理,实现了线程间的同步和协调。理解AQS的工作原理和应用场景,对于编写高效、可靠的并发程序具有重要意义。在面试中,能够清晰地阐述AQS的核心思想、工作流程和应用场景,将有助于提高面试的通过率。