文章目录
- 什么是 AQS
- AQS 的工作原理
- 同步状态(state)
- 等待队列
- AQS 是如何让线程排队并唤醒的
- 公平锁和非公平锁
- AQS 的应用场景
- ReentrantLock(可重入锁)
- AQS 在 ReentrantLock 中的工作原理
- 典型应用场景
- CountDownLatch(倒计时器)
- AQS 在 CountDownLatch 中的工作原理
- 典型应用场景
- Semaphore(信号量)
- AQS 在 Semaphore 中的工作原理
- 典型应用场景
- CyclicBarrier(循环栅栏)
- AQS 的相关实现原理
- 典型应用场景
什么是 AQS
AQS 是一个用于实现线程同步的框架。假设多个线程在争抢某个资源(比如一把锁),AQS 负责决定哪个线程能够拿到资源,哪些线程需要等待,并保证在多个线程同时竞争时不会出错。
举个例子:
想象有一台游戏机,多个孩子(线程)想要玩游戏,但每次只能有一个孩子使用。其他孩子需要排队,等前面的孩子玩完之后,才能依次接着玩。AQS 就像一个 排队管理系统,负责让孩子们按顺序使用游戏机(资源)。
AQS 的工作原理
AQS 是如何管理线程竞争资源的呢?它的核心机制有两个:
- 同步状态(state):表示资源是否可用。
- 等待队列:当资源不可用时,线程进入等待队列,按顺序排队。
同步状态(state)
AQS 使用一个整数变量 state
来表示资源的状态。举个简单的例子,假设 state = 0
表示资源(例如锁)是空闲的,而 state = 1
表示资源已被占用。
当线程来获取资源时,AQS 会检查 state
是否为 0。
- 如果是 0,说明资源可用,线程可以成功获取资源,并将
state
设置为 1,表示资源被占用了。 - 如果
state
是 1,说明资源已经被其他线程占用了,这个线程无法直接获取资源,而是进入 等待队列。
等待队列
当资源被占用时,线程会排队等候。AQS 内部维护了一个 等待队列,就像现实中的排队一样,谁先来谁先排队。
这个队列是一个 双向链表(双向链表就是每个节点都可以知道前一个和后一个节点),等待获取资源的线程被封装成队列中的节点,当资源释放时,AQS 会按照队列的顺序唤醒等待的线程,让它们尝试再次获取资源。
AQS 是如何让线程排队并唤醒的
现在让我们看看 AQS 是如何处理线程获取资源的过程。当多个线程想获取同一个资源时,AQS 会执行以下几步:
- 线程尝试获取资源:当一个线程想获取资源时,AQS 首先会检查资源的状态(
state
)。- 如果资源空闲(
state = 0
),线程成功获取资源,state
会变为 1。 - 如果资源已被占用(
state = 1
),线程会进入等待队列,等待资源被释放。
- 如果资源空闲(
- 等待队列的工作方式:
- 如果线程需要等待资源(因为资源已被其他线程占用),它会被放入等待队列。这个等待队列会按照 **先进先出(FIFO)**的顺序排队,先进入队列的线程会先被唤醒。
- 线程进入等待队列后,AQS 会让这个线程进入一种“休眠”状态,直到它被唤醒,才可以继续运行。
- 释放资源并唤醒下一个线程:当某个线程用完资源后,它会“释放资源”,即将
state
变回 0,表示资源可用。AQS 会检查等待队列,唤醒队列中排在最前面的线程,让它重新尝试获取资源。
举个例子:
假设有两个人(线程)A 和 B 想要玩同一台游戏机(资源),游戏机每次只能给一个人玩:
- A 先到,看到游戏机空闲(
state = 0
),于是他拿到游戏机(state = 1
)。 - B 也来了,但发现游戏机正在被 A 玩(
state = 1
),于是 B 只能排队等待。 - 当 A 玩完了,他把游戏机放回去,游戏机变为空闲状态(
state = 0
),这时 AQS 会通知 B:“你可以继续玩了”,B 被唤醒,成功拿到游戏机玩。
公平锁和非公平锁
AQS 支持两种排队机制:公平锁和非公平锁。
公平锁:按照队列的顺序让线程获取资源,谁先排队谁先得。就像银行排队,先来的人先办理业务。
非公平锁:新来的线程即使排在队列后面,也可以直接尝试抢资源,有时候能插队成功。这种方式可以提高性能,但可能导致一些线程长时间得不到资源。
AQS 的应用场景
AbstractQueuedSynchronizer
(AQS) 是 Java 并发工具包中的核心基础,许多常用的并发控制工具都基于 AQS 实现。AQS 通过状态变量(state)和线程等待队列管理线程的同步操作。在实际开发中,AQS 主要为以下几种并发工具提供支持:
- ReentrantLock(可重入锁)
- CountDownLatch(倒计时器)
- Semaphore(信号量)
- CyclicBarrier(循环栅栏)
ReentrantLock(可重入锁)
ReentrantLock
是 Java 中非常常用的显式锁,它允许同一个线程多次获取同一把锁,而不会出现死锁问题。也就是说,线程可以“重入”该锁(同一线程多次持有)。这个锁可以选择 公平锁 和 非公平锁,确保不同线程对锁的竞争方式。
AQS 在 ReentrantLock 中的工作原理
AQS 通过 state
来表示锁的持有状态,state = 0
表示锁是空闲的,state > 0
表示锁已被占用。
当线程尝试获取锁时,AQS 会检查当前 state
是否为 0。
- 如果锁空闲,线程能够成功获取锁,并且
state
变为 1。 - 如果锁已经被占用,线程会进入 AQS 的等待队列,等待锁被释放。
当同一个线程多次获取锁时,AQS 会将 state
增加(重入锁的概念)。当线程释放锁时,state
递减,直到 state = 0
,锁才真正释放,其他等待的线程才有机会获取锁。
典型应用场景
控制对共享资源的独占访问:例如在多线程环境中,确保同一时刻只有一个线程可以对共享数据(如某个对象或文件)进行修改,避免线程竞争造成的数据不一致或资源冲突。
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码,只能有一个线程访问
} finally {
lock.unlock();
}
CountDownLatch(倒计时器)
CountDownLatch
是一种同步工具,允许一个或多个线程等待其他线程完成某些操作后再继续执行。例如,你可以让主线程等待所有工作线程执行完任务后再继续工作。CountDownLatch
内部依赖 AQS 来管理线程的等待和唤醒。
AQS 在 CountDownLatch 中的工作原理
AQS 使用 state
来表示倒计时的初始值。例如,如果你设置 CountDownLatch
为 3,state
就初始为 3。
当某个线程调用 countDown()
时,state
会减少 1。每个线程完成任务时都调用 countDown()
,表示任务的完成。
当 state
减到 0 时,所有在 await()
方法上等待的线程都会被唤醒,并且可以继续执行。
典型应用场景
协调多个线程并发执行:例如,当多个线程并行处理不同任务,主线程需要等待所有子任务都完成后才能继续。你可以使用 CountDownLatch
来实现这一点。
服务启动顺序管理:在分布式系统中,某些模块可能需要等待其他模块先启动,确保它们依赖的服务已准备好。
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行任务
latch.countDown(); // 任务完成,倒计时减 1
}).start();
}
latch.await(); // 主线程等待,直到倒计时为 0
System.out.println("所有任务已完成");
Semaphore(信号量)
Semaphore
是一种限制多个线程并发访问资源的工具,它通过 AQS 来管理可以同时访问资源的线程数量。Semaphore
非常适合控制对有限资源的访问,比如数据库连接池、线程池等。
AQS 在 Semaphore 中的工作原理
Semaphore
通过 AQS 的 state
来表示剩余的可用资源数量(即许可数)。初始的 state
值表示信号量的许可数(允许多少线程同时访问资源)。
当线程调用 acquire()
方法时,AQS 会检查 state
是否大于 0:
- 如果
state > 0
,线程可以成功获取资源,state
减少。 - 如果
state = 0
,线程进入等待队列,等待其他线程释放资源(即release()
)。
当线程调用 release()
释放资源时,state
增加,唤醒等待的线程来获取资源。
典型应用场景
限流和流量控制:在并发系统中,Semaphore
常用于限制同时访问某个服务或资源的线程数,防止系统过载。例如,限制同时执行的数据库查询数量,以避免过多并发请求导致服务器压力过大。
连接池管理:比如控制对数据库连接池的访问,确保不超过最大连接数。
Semaphore semaphore = new Semaphore(3); // 允许最多 3 个线程同时访问
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
// 访问共享资源
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
CyclicBarrier(循环栅栏)
CyclicBarrier
是一种允许一组线程相互等待,直到所有线程都到达某个屏障后才继续执行的工具。与 CountDownLatch
不同,CyclicBarrier
可以重复使用,适合用于需要多轮次的同步操作场景。
AQS 的相关实现原理
尽管 CyclicBarrier
并没有直接使用 AQS 实现,但是它和 AQS 的等待机制类似。多个线程在调用 await()
时,会等待其他线程到达同一屏障点。当所有线程都到达屏障时,才能继续往下执行下一步操作。
典型应用场景
并行计算任务中的同步点:多个线程分工合作处理一个大任务,每个线程负责一部分任务。当所有线程都处理完各自的部分后,需要在某个同步点上汇总数据,并继续进行下一步工作。
多人游戏同步:在网络游戏中,多个玩家同时进行某一阶段游戏,所有玩家到达某个阶段的同步点后才能进入下一阶段。
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障,继续执行...");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 到达屏障");
try {
barrier.await(); // 等待其他线程
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}