AQS详情

AQS概述

AQS(AbstractQueuedSynchronizer)是Java中并发编程的关键类之一,它提供了一种用于构建锁和其他同步器的框架。AQS内部维护了一个FIFO双向队列,用来管理获取同步状态失败的线程,并且通过内置的状态来控制线程的阻塞和唤醒。

AQS 详解

AQS 是 AbustactQueuedSynchronizer 的简称,它是一个 Java 提高的底层同步工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管理这个同步状态。AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS的
AQS 是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、CountDownLatch、Semaphore 等都用到了 AQS.从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和共享锁。
排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。
共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能

AQS的核心思想是使用一个整型的state变量来表示同步状态,当state为0时表示没有线程获取到同步状态,大于0表示有线程获取到同步状态,小于0通常表示有线程在等待获取同步状态。具体来说,AQS提供了以下几个主要方法:

  1. acquire(int arg):尝试获取同步状态,如果获取失败则会进入队列进行阻塞等待。
  2. release(int arg):释放同步状态,可能会唤醒队列中等待的线程。
  3. acquireShared(int arg):尝试获取共享同步状态,如果获取失败则会进入队列进行阻塞等待。
  4. releaseShared(int arg):释放共享同步状态,可能会唤醒队列中等待的线程。
    AQS为我们提供了一个非常灵活的同步状态管理框架,使得我们可以相对容易地实现各种同步器,比如ReentrantLock、CountDownLatch、Semaphore等。通过扩展AQS并实现相应的获取和释放同步状态的方法,我们可以定制符合自己需求的同步器。
    总的来说,AQS为并发编程提供了强大的基础设施,能够帮助我们更好地理解和实现各种复杂的同步机制。

AQS(AbstractQueuedSynchronizer)是Java并发包java.util.concurrent.locks中的一个核心类,它提供了一种基于FIFO(先进先出)队列的阻塞锁和相关的同步器(如ReentrantLock、Semaphore、CountDownLatch等)的实现框架。AQS被设计为一个依赖状态的同步器,它使用一个整数来表示同步状态,这个状态可以通过原子操作进行更新。
AQS的主要功能包括:

  1. 同步状态管理:AQS使用一个int类型的变量来保存同步状态,该状态可以被获取、设置或者原子地比较并设置。
  2. FIFO队列:AQS维护了一个FIFO队列,用于管理等待获取同步状态的线程。当一个线程尝试获取同步状态失败时,它会被放入队列中等待。
  3. 可重入锁:AQS支持可重入锁的特性,即同一个线程可以多次获取同步状态。
  4. 条件变量:AQS提供了与Object.wait()和Object.notify()类似的功能,但更加灵活和高效。通过继承AQS的子类,可以定义自己的条件变量。
    AQS的实现非常精巧,它利用了Java的Unsafe类提供的原子操作来确保对同步状态的安全访问。同时,AQS还提供了大量的模板方法,使得开发者可以方便地扩展和定制自己的同步器。
    以下是一些基于AQS实现的常见同步器:
    ● ReentrantLock:一个可重入的互斥锁,它支持公平锁和非公平锁。
    ● ReentrantReadWriteLock:一个可重入的读写锁,它允许多个读线程同时访问共享资源,但写线程会阻塞读线程和其他写线程。
    ● Semaphore:一个信号量,它用于控制同时访问某个特定资源的线程数量。
    ● CountDownLatch:一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。
    ● CyclicBarrier:一个可以让一组线程互相等待,直到所有线程都到达某个公共屏障点的同步辅助类。
    这些同步器都通过继承AQS并实现其模板方法来实现各自的功能。

AQS 支持两种同步方式

(1)独占式
(2)共享式
这样方便使用者实现不同类型的同步组件,独占式如 ReentrantLock,共享式如 Semaphore,
CountDownLatch,组 合 式 的 如 ReentrantReadWriteLock。总之,AQS 为使用提供了底层支撑,如何组装实现,使用者可以自由发挥
AQS中有个状态维护字段state,volatile修饰,保证可见性,代表线程是否抢占到锁,或者锁的重入次数
AQS中有一个CLH队列,是一个FIFO的双向链表,存着需要抢占锁的线程、等待状态等信息。
AQS的典型的设计模式是模板方法模式,提供tryAcquire等钩子方法,给予不同子类不同的实现。
AQS中唤醒线程源码在release方法,唤醒的是head的next节点的线程,而不是随机

AQS为什么要用双向链表

首先,双向链表有两个指针,一个指针指向前置节点,一个指针指向后继节点。所以,双向链表可以支持常量 O(1) 时间复杂度的情况下找到前驱节点。因此,双向链表在插入和删除操作的时候,要比单向链表简单、高效。从双向链表的特性来看,我认为 AQS 使用双向链表有三个方面的原因

第1个原因,没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。
所以,线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从 Head节点开始遍历,性能非常低

第2个原因,在 Lock 接口里面有一个,lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。
也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。这个时候,被中断的线程的状态会修改成 CANCELLED。而被标记为CANCELLED 状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。
这就意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。在这种情况下,如果是单向链表,就需要从 Head 节点开始往下逐个遍历,找到并移除异常状态的节点。同样效率也比较低,还会导致锁唤醒的操作和遍历操作之间的竞争

第3个原因,是为了避免线程阻塞和唤醒的开销,所以刚加入到链表的线程,首先会通过自旋的方式尝试去竞争锁。但是实际上按照公平锁的设计,只有头节点的下一个节点才有必要去竞争锁,后续的节点竞争锁的意义不大。否则,就会造成羊群效应,也就是大量的线程在阻塞之前尝试去竞争锁带来比较大的性能开销。

所以,为了避免这个问题,加入到链表中的节点在尝试竞争锁之前,需要判断前置节点是不是头节点,如果不是头节点,就没必要再去触发锁竞争的动作。所以这里会涉及到前置节点的查找,如果是单向链表,那么这个功能的实现会非常复杂。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

思静语

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值