深入浅出 AbstractQueuedSynchronizer (AQS)

在这里插入图片描述

什么是 AQS

AQS 是一个用于实现线程同步的框架。假设多个线程在争抢某个资源(比如一把锁),AQS 负责决定哪个线程能够拿到资源,哪些线程需要等待,并保证在多个线程同时竞争时不会出错。

举个例子:

想象有一台游戏机,多个孩子(线程)想要玩游戏,但每次只能有一个孩子使用。其他孩子需要排队,等前面的孩子玩完之后,才能依次接着玩。AQS 就像一个 排队管理系统,负责让孩子们按顺序使用游戏机(资源)。

AQS 的工作原理

AQS 是如何管理线程竞争资源的呢?它的核心机制有两个:

  1. 同步状态(state):表示资源是否可用。
  2. 等待队列:当资源不可用时,线程进入等待队列,按顺序排队。

同步状态(state)

AQS 使用一个整数变量 state 来表示资源的状态。举个简单的例子,假设 state = 0 表示资源(例如锁)是空闲的,而 state = 1 表示资源已被占用。

当线程来获取资源时,AQS 会检查 state 是否为 0。

  • 如果是 0,说明资源可用,线程可以成功获取资源,并将 state 设置为 1,表示资源被占用了。
  • 如果 state 是 1,说明资源已经被其他线程占用了,这个线程无法直接获取资源,而是进入 等待队列

等待队列

当资源被占用时,线程会排队等候。AQS 内部维护了一个 等待队列,就像现实中的排队一样,谁先来谁先排队。

这个队列是一个 双向链表(双向链表就是每个节点都可以知道前一个和后一个节点),等待获取资源的线程被封装成队列中的节点,当资源释放时,AQS 会按照队列的顺序唤醒等待的线程,让它们尝试再次获取资源。

AQS 是如何让线程排队并唤醒的

现在让我们看看 AQS 是如何处理线程获取资源的过程。当多个线程想获取同一个资源时,AQS 会执行以下几步:

  1. 线程尝试获取资源:当一个线程想获取资源时,AQS 首先会检查资源的状态(state)。
    • 如果资源空闲(state = 0),线程成功获取资源,state 会变为 1。
    • 如果资源已被占用(state = 1),线程会进入等待队列,等待资源被释放。
  2. 等待队列的工作方式:
    • 如果线程需要等待资源(因为资源已被其他线程占用),它会被放入等待队列。这个等待队列会按照 **先进先出(FIFO)**的顺序排队,先进入队列的线程会先被唤醒。
    • 线程进入等待队列后,AQS 会让这个线程进入一种“休眠”状态,直到它被唤醒,才可以继续运行。
  3. 释放资源并唤醒下一个线程:当某个线程用完资源后,它会“释放资源”,即将 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 主要为以下几种并发工具提供支持:

  1. ReentrantLock(可重入锁)
  2. CountDownLatch(倒计时器)
  3. Semaphore(信号量)
  4. 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();
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值