AQS(AbstractQueuedSynchronizer)是 Java 中的一个抽象同步框架。
主要功能和特点:
- 同步器基础:AQS 为实现同步器提供了通用的基础框架,许多同步类如
ReentrantLock
、Semaphore
、CountDownLatch
等都是基于 AQS 构建的。 - 状态管理:通过一个整数变量来表示同步状态,可以根据不同的同步需求对这个状态进行修改和检查。例如,在独占锁中,0 表示锁未被占用,1 表示锁已被占用;在共享锁中,这个状态值可以表示可用的共享资源数量。
- 队列管理:维护一个等待队列,当线程获取同步状态失败时,会被封装成节点加入等待队列中。等待队列采用 FIFO(先进先出)的方式管理线程,保证公平性。
- 阻塞和唤醒机制:使用 LockSupport 工具类来实现线程的阻塞和唤醒操作。当线程获取同步状态失败时,会被阻塞等待;当同步状态可用时,会唤醒等待队列中的一个或多个线程。
AQS 的设计非常精巧,它将同步状态的管理、等待队列的维护以及阻塞和唤醒机制封装在一个抽象类中,使得开发者可以方便地基于它实现各种同步器,同时也提高了同步类的可扩展性和可维护性。
以下是对基于 AQS 的几个同步类的详细讲解及示例代码:
一、ReentrantLock
ReentrantLock
是一个可重入的互斥锁,它实现了Lock
接口。
- 基本用法:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
try {
// 获取锁
lock.lock();
System.out.println("Locked.");
} finally {
// 释放锁
lock.unlock();
}
}
}
说明:这段代码展示了ReentrantLock
的基本使用方法。首先创建一个ReentrantLock
实例。在try
块中,通过调用lock.lock()
获取锁。在finally
块中,确保无论是否发生异常都调用lock.unlock()
释放锁,以防止出现死锁。
- 可重入性:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockRecursiveExample {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
try {
lock.lock();
System.out.println("First level lock acquired.");
methodWithLock();
} finally {
lock.unlock();
}
}
private static void methodWithLock() {
try {
lock.lock();
System.out.println("Second level lock acquired.");
} finally {
lock.unlock();
}
}
}
说明:在这个例子中,展示了ReentrantLock
的可重入性。在main
方法中获取了锁后,调用methodWithLock
方法,该方法又再次获取同一个锁。由于ReentrantLock
是可重入的,所以不会出现死锁。在释放锁时,需要确保每个获取锁的地方都有相应的释放操作,以保证锁的正确释放。
在ReentrantLock
内部,它使用了 AQS 的状态变量来表示锁的持有情况。当一个线程获取锁时,状态变量会增加;释放锁时,状态变量会减少。如果状态为 0,表示锁未被占用;如果状态大于 0,表示锁被占用,且值表示重入的次数。
二、Semaphore
Semaphore
可以控制同时访问某个资源的线程数量。
- 基本用法:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 5; i++) {
new Thread(new Worker(semaphore)).start();
}
}
static class Worker implements Runnable {
private Semaphore semaphore;
public Worker(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " acquired permit.");
// 模拟使用资源
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}
}
说明:这段代码创建了一个Semaphore
实例,初始许可数量为 3。然后创建了 5 个线程,每个线程在执行时,首先尝试获取一个许可(通过semaphore.acquire()
)。如果有可用许可,线程会继续执行并输出获取到许可的信息,然后模拟使用资源(这里通过Thread.sleep
模拟)。最后,在finally
块中释放许可(通过semaphore.release()
)。Semaphore
通过 AQS 来管理许可的分配和释放,维护一个等待队列,当许可不足时,线程会被加入等待队列并阻塞,直到有可用许可被释放。
以下是一些关于 AQS 的面试题:
一、基础概念类
-
请解释什么是 AQS?
- AQS(AbstractQueuedSynchronizer)是 Java 中的一个抽象同步框架,为实现同步器提供了通用的基础架构,许多同步类如
ReentrantLock
、Semaphore
、CountDownLatch
等都是基于 AQS 构建的。
- AQS(AbstractQueuedSynchronizer)是 Java 中的一个抽象同步框架,为实现同步器提供了通用的基础架构,许多同步类如
-
AQS 主要解决了什么问题?
- 提供了一种通用的同步机制,使得开发者可以方便地实现各种同步器,解决了多线程环境下的资源同步和互斥问题。
-
AQS 中的同步状态是如何表示的?
- 通过一个整数变量来表示同步状态,可以根据不同的同步需求对这个状态进行修改和检查。
二、队列管理类
-
AQS 中的等待队列是如何工作的?
- 当线程获取同步状态失败时,会被封装成节点加入等待队列中。等待队列采用 FIFO(先进先出)的方式管理线程,保证公平性。
-
等待队列中的节点有哪些状态?
- 等待队列中的节点有以下几种状态:
- CANCELLED:表示节点已被取消。
- SIGNAL:表示后继节点需要被唤醒。
- CONDITION:表示节点在条件队列中。
- PROPAGATE:表示下一次共享式同步状态获取将会无条件地传播下去。
- 等待队列中的节点有以下几种状态:
-
AQS 如何保证等待队列的公平性?
- AQS 通过在获取同步状态时判断等待队列中是否有前驱节点,如果有则排队等待,从而保证了公平性。
4.在 AQS(AbstractQueuedSynchronizer)中,为什么等待队列的遍历通常从尾节点开始向前遍历?
性能考虑
-
快速找到等待时间最长的节点:
- 从后往前遍历可以快速定位到等待时间最长的节点。在一些场景下,比如公平锁的实现中,需要按照等待时间的先后顺序来分配锁资源。从尾节点开始遍历可以更快地找到最应该被唤醒的节点,减少等待时间,提高系统的响应性能。
- 当有新的线程尝试获取锁失败并加入等待队列时,从尾节点开始遍历可以更高效地确定新节点在队列中的正确位置,避免了从头部开始遍历整个队列的开销。
-
减少不必要的遍历:
- 在某些操作中,如判断是否有线程等待、唤醒节点等,从尾节点开始遍历可以在较短的时间内确定是否需要进行进一步的操作。如果从头部开始遍历,可能需要遍历整个队列才能得出结论,而从尾节点开始可以更快地判断出一些特定的情况,减少不必要的遍历次数。
队列结构和操作的特点
-
与节点添加和删除的顺序相关:
- 在 AQS 的等待队列中,新节点通常添加到尾部。从尾节点开始遍历可以更好地与节点的添加操作相配合,方便地检查和处理新加入的节点。例如,在判断是否需要阻塞当前线程时,可以从尾节点开始向前查找是否有前驱节点持有锁,如果有则当前线程需要阻塞等待。
- 当节点被唤醒或取消时,通常需要对队列进行调整。从尾节点开始遍历可以更方便地处理这些情况,确保队列的正确性和有效性。
-
符合 FIFO(先进先出)原则:
- 虽然 AQS 的等待队列是双向链表结构,但从整体的操作逻辑来看,从尾节点开始遍历也符合先进先出的原则。新加入的线程等待在队列尾部,而被唤醒的线程通常是等待时间最长的头部节点的后继节点。从尾节点开始遍历可以更好地维护这种 FIFO 的顺序,确保公平性。
三、并发环境下的稳定性和可靠性
-
减少竞争和冲突:
- 在高并发环境下,多个线程可能同时对等待队列进行操作。从尾节点开始遍历可以减少对头部节点的竞争,因为头部节点通常是被频繁访问和操作的对象。通过从尾节点开始,可以分散对队列的访问,降低竞争和冲突的可能性,提高系统的稳定性。
- 在一些同步操作中,如唤醒节点,从尾节点开始遍历可以避免对头部节点的过度依赖,减少因为头部节点的竞争而导致的性能下降和不稳定情况。
-
更好地处理异常情况:
- 在并发环境下,可能会出现节点被意外取消或出现异常的情况。从尾节点开始遍历可以更方便地检测和处理这些异常情况,确保队列的完整性和正确性。例如,如果发现某个节点被取消,可以从尾节点开始向前查找其前驱节点,并进行相应的调整,以保证队列的连续性和有效性。
三、同步机制类
-
AQS 是如何实现线程的阻塞和唤醒的?
- 使用
LockSupport
工具类来实现线程的阻塞和唤醒操作。当线程获取同步状态失败时,会被阻塞等待;当同步状态可用时,会唤醒等待队列中的一个或多个线程。
- 使用
-
独占式同步和共享式同步有什么区别?
- 独占式同步在同一时刻只有一个线程可以获取同步状态,而共享式同步在同一时刻可以有多个线程获取同步状态。
-
AQS 中的 tryAcquire 和 tryRelease 方法的作用是什么?
tryAcquire
方法用于尝试获取独占式同步状态,如果获取成功则返回 true,否则返回 false。tryRelease
方法用于尝试释放独占式同步状态,如果释放成功则返回 true,否则返回 false。
四、高级应用类
-
请举例说明如何基于 AQS 实现一个自定义的同步器?
- 可以通过继承 AQS 并实现其抽象方法来实现自定义的同步器。例如,实现一个自定义的锁,可以重写
tryAcquire
、tryRelease
和isHeldExclusively
等方法。
- 可以通过继承 AQS 并实现其抽象方法来实现自定义的同步器。例如,实现一个自定义的锁,可以重写
-
在实际项目中,你使用过哪些基于 AQS 的同步类?它们分别解决了什么问题?
- 例如
ReentrantLock
用于替代传统的synchronized
关键字,提供了更灵活的锁机制;Semaphore
用于控制同时访问某个资源的线程数量;CountDownLatch
用于等待多个线程完成任务后再继续执行。
- 例如