什么是AQS

目录

AQS

介绍

原理

[以可重入的互斥锁 ReentrantLock 为例](#以可重入的互斥锁 ReentrantLock 为例)

[以倒计时器 CountDownLatch 以例](#以倒计时器 CountDownLatch 以例)

[AQS 资源共享方式](#AQS 资源共享方式)

实现自定义同步器

示例

性能优化


AQS

介绍

AQS (AbstractQueuedSynchronizer ),抽象队列同步器。AQS 是一个功能强大且灵活的框架,适合于实现高性能的同步工具。这个类在 java.util.concurrent.locks 包下面。AQS 就是一个抽象类,主要用来构建锁和同步器。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {}

AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如ReentrantLockSemaphoreReentrantReadWriteLockSynchronousQueue等等皆是基于 AQS 的。

原理

AQS 核心思想是如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 实现的。

CLH 锁是对自旋锁的一种改进,是一个虚拟的双向队列(即不存在队列实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。

AQS 使用 int 成员变量 state 表示同步状态,通过内置的 FIFO 线程等待/等待队列 来完成获取资源线程的排队工作。

  • 状态信息 state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。
  • state 可以通过 protected 类型的getState()setState()compareAndSetState() 进行操作。并且,这几个方法都是 final 修饰的,在子类中无法被重写。

以可重入的互斥锁 ReentrantLock 为例

以可重入的互斥锁 ReentrantLock 为例,它的内部维护了一个 state 变量,用来表示锁的占用状态。state 的初始值为 0,表示锁处于未锁定状态。

当线程 A 调用 lock() 方法时,会尝试通过 tryAcquire() 方法独占该锁,并让 state 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 队列)中,直到其他线程释放该锁。

假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 state 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。

以倒计时器 CountDownLatch 以例

再以倒计时器 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程开始执行任务,每执行完一个子线程,就调用一次 countDown() 方法。该方法会尝试使用 CAS(Compare and Swap) 操作,让 state 的值减少 1。

当所有的子线程都执行完毕后(即 state 的值变为 0),会调用 CountDownLatch.unpark()唤醒主线程。这时,主线程就可以从 CountDownLatch.await() 返回,继续执行后续的操作。

AQS 资源共享方式

AQS 定义两种资源共享方式:

  • Exclusive独占,只有一个线程能执行,如ReentrantLock
  • Share共享,多个线程可同时执行,如Semaphore/CountDownLatch

一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

实现自定义同步器

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。
  2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法

//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)

//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)

//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)

//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)

//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()

开发者可以通过继承 AQS 类并实现其中的方法(如 tryAcquire()tryRelease()tryAcquireShared()tryReleaseShared())来创建自定义的锁或其他同步工具。

什么是钩子方法呢? 钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。

示例

下面是一个简单的示例,展示如何使用 AQS 创建一个独占锁。

java 复制代码
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class MyLock {
    private static class Sync extends AbstractQueuedSynchronizer {
        // 尝试获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) { // 如果状态为0,则设置为1
                setExclusiveOwnerThread(Thread.currentThread()); // 记录当前线程
                return true;
            }
            return false;
        }

        // 尝试释放锁
        @Override
        protected boolean tryRelease(int arg) {
            if (getExclusiveOwnerThread() != Thread.currentThread()) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null); // 清空持有线程
            setState(0); // 重置状态
            return true;
        }

        // 判断锁是否被持有
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }
}

性能优化

AQS 在多线程竞争情况下,通过使用自旋锁和阻塞等技术来优化性能,减少上下文切换的开销。

相关推荐
最爱番茄味4 分钟前
Python实例之函数基础打卡篇
开发语言·python
zjw_rp26 分钟前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob39 分钟前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder1 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
engchina1 小时前
如何在 Python 中忽略烦人的警告?
开发语言·人工智能·python
向宇it1 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行1 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
诚丞成1 小时前
计算世界之安生:C++继承的文水和智慧(上)
开发语言·c++
Smile灬凉城6662 小时前
反序列化为啥可以利用加号绕过php正则匹配
开发语言·php
lsx2024062 小时前
SQL MID()
开发语言