写点什么

探索抽象同步队列 AQS

作者:emanjusaka
  • 2023-09-25
    湖南
  • 本文字数:4271 字

    阅读完需:约 14 分钟

探索抽象同步队列 AQS

by emanjusaka from ​ https://www.emanjusaka.top/archives/8 彼岸花开可奈何

本文欢迎分享与聚合,全文转载请留下原文地址。

前言

AbstractQueuedSynchronizer 抽象同步队列简称 AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用 AQS 实现的。大多数开发者可能永远不会直接使用 AQS,但是知道其原理对于架构设计还是很有帮助的。AQS 是 Java 中的一个抽象类,全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它定义了两种资源共享模式:独占式和共享式。独占式每次只能有一个线程持有锁,例如 ReentrantLock 实现的就是独占式的锁资源;共享式允许多个线程同时获取锁,并发访问共享资源,ReentrantReadWriteLock 和 CountDownLatch 等就是实现的这种模式。AQS 维护了一个 volatile 的 state 变量和一个 FIFO(先进先出)的队列。其中 state 变量代表的是竞争资源标识,而队列代表的是竞争资源失败的线程排队时存放的容器 。

一、原理

AQS 的核心思想是通过一个 FIFO 的队列来管理线程的等待和唤醒,同时维护了一个 state 变量来表示同步状态,可以通过 getState、setState、compareAndSetState 函数修改其值。当一个线程想要获取锁时,如果 state 为 0,则表示该线程获取锁成功,否则表示该线程获取锁失败。它将被放入等待队列中,直到满足特定条件才能再次尝试获取。当一个线程释放锁时,如果 state 为 1,则表示该线程释放锁成功,否则表示该线程释放锁失败。AQS 通过 CAS 操作来实现加锁和解锁。

1.1 CLH 队列


AQS 中的 CLH 队列锁是 CLH 锁的一种变体,将自旋操作改成了阻塞线程操作。AQS 中的对 CLH 锁数据结构的改进主要包括三方面:扩展每个节点的状态、显式的维护前驱节点和后继节点以及诸如出队节点显式设为 null 等辅助 GC 的优化。


在 AQS(AbstractQueuedSynchronizer)中使用的 CLH 队列,head 指针和 tail 指针分别指向 CLH 队列中的两个关键节点。


  1. head 指针:head 指针指向 CLH 队列中的首个节点,该节点表示当前持有锁的线程。当一个线程成功地获取到锁时,它就成为了持有锁的线程,并且会将该信息记录在 head 指针所指向的节点中。

  2. tail 指针:tail 指针指向 CLH 队列中的最后一个节点,该节点表示队列中最后一个等待获取锁的线程。当一个线程尝试获取锁时,它会生成一个新的节点,并将其插入到 CLH 队列的尾部,然后成为 tail 指针所指向的节点。这样,tail 指针的作用是标记当前 CLH 队列中最后一个等待获取锁的线程。


通过 head 指针和 tail 指针,CLH 队列能够维护一种有序的等待队列结构,保证线程获取锁的顺序和互斥访问的正确性。当一个线程释放锁时,它会修改当前节点的状态,并唤醒后继节点上的线程,让后续的线程能够及时感知锁的释放,并争夺获取锁的机会。

1.2 线程同步

对于 AQS 来说,线程同步的关键是对状态值 state 进行操作。state 为 0 时表示没有线程持有锁,大于 0 时表示有线程持有锁。根据 state 是否属于一个线程,操作 state 的方式分为独占方式和共享方式。


在独占方式下获取和释放资源使用的方法为:


  • void acquire(int arg)

  • void acquireInterruptibly(int arg)

  • boolean release(int arg)


使用独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作 state 获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。


在共享方式下获取和释放资源的方法为:


  • void acquireShared(int arg)

  • voidacquireSharedInterruptibly(int arg)

  • boolean releaseShared(int arg)。


对应共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过 CAS 方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用 CAS 方式进行获取即可。

二、资源获取与释放

2.1 独占式

  1. 当一个线程调用 acquire(int arg)方法获取独占资源时,会首先使用 tryAcquire 方法尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回,失败则将当前线程封装为类型为 Node.EXCLUSIVE 的 Node 节点后插入到 AQS 阻塞队列的尾部,并调用 LockSupport.park(this)方法挂起自己。


        public final void acquire(int arg) {          if (! tryAcquire(arg) &&              acquireQueued(addWaiter(Node.EXCLUSIVE), arg))              selfInterrupt();       }
复制代码


  1. 当一个线程调用 release(int arg)方法时会尝试使用 tryRelease 操作释放资源,这里是设置状态变量 state 的值,然后调用 LockSupport.unpark(thread)方法激活 AQS 队列里面被阻塞的一个线程(thread)。被激活的线程则使用 tryAcquire 尝试,看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。


        public final boolean release(int arg) {            if (tryRelease(arg)) {                Node h = head;                if (h ! = null && h.waitStatus ! = 0)                    unparkSuccessor(h);                return true;            }            return false;        }
复制代码

2.2 共享式

  1. 当线程调用 acquireShared(int arg)获取共享资源时,会首先使用 tryAcquireShared 尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回,失败则将当前线程封装为类型为 Node.SHARED 的 Node 节点后插入到 AQS 阻塞队列的尾部,并使用 LockSupport.park(this)方法挂起自己。


        public final void acquireShared(int arg) {            if (tryAcquireShared(arg) < 0)                doAcquireShared(arg);        }
复制代码


  1. 当一个线程调用 releaseShared(int arg)时会尝试使用 tryReleaseShared 操作释放资源,这里是设置状态变量 state 的值,然后使用 LockSupport.unpark(thread)激活 AQS 队列里面被阻塞的一个线程(thread)。被激活的线程则使用 tryReleaseShared 查看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。


        public final boolean releaseShared(int arg) {              if (tryReleaseShared(arg)) {                  doReleaseShared();                  return true;              }              return false;          }
复制代码

三、基于 AQS 实现自定义同步器

基于 AQS 实现一个不可重入的独占锁,自定义 AQS 需要重写一系列函数,还需要定义原子变量 state 的含义。这里定义,state 为 0 表示目前锁没有被线程持有,state 为 1 表示锁已经被某一个线程持有,由于是不可重入锁,所以不需要记录持有锁的线程获取锁的次数。


package top.emanjusaka;
import java.io.Serializable;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.AbstractQueuedSynchronizer;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;
public class UnReentrantLock implements Lock, Serializable { // 借助 AbstractQueuedSynchronizer 实现 private static class Sync extends AbstractQueuedSynchronizer { // 查看是否有线程持有锁 protected boolean isHeldExclusively() { return getState() == 1; } // 尝试获取锁 public boolean tryAcquire(int acquires) { assert acquires == 1; // 使用CAS 设置state if (compareAndSetState(0, 1)) { // 如果 CAS 操作成功,表示成功获得了锁。这时,通过 setExclusiveOwnerThread 方法将当前线程设置为独占锁的拥有者 setExclusiveOwnerThread(Thread.currentThread()); return true; } // 如果 CAS 操作失败,即无法将 state 的值从 0 设置为 1,表示锁已被其他线程占用,无法获取锁,于是返回 false。 return false; } // 尝试释放锁 protected boolean tryRelease(int releases) { assert releases == 1; if (getState() == 0) throw new IllegalMonitorStateException(); // 释放成功,将独占锁的拥有者设为null setExclusiveOwnerThread(null); // 将state的值设为0 setState(0); return true; }
Condition newCondition() { return new ConditionObject(); } }
private final Sync sync = new Sync();
@Override public void lock() { sync.acquire(1); }
@Override public boolean tryLock() { return sync.tryAcquire(1); }
@Override public void unlock() { sync.release(1); }
@Override public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.isHeldExclusively(); }
public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); }
}
复制代码


在释放锁时并没有使用 CAS(Compare and Swap)操作,而是直接使用了 setState 方法来将 state 的值设置为 0。


这是因为在释放锁的过程中,并不需要涉及到多线程并发的问题。只有持有锁的线程才能够释放锁,其他线程无法对锁进行操作。因此,不需要使用 CAS 来进行原子性的状态更新。


在这种情况下,可以直接使用普通的方法来设置 state 的值为 0,将独占锁的拥有者设为 null。因为只有一个线程可以操作这个锁,不存在并发竞争的情况,也就不需要使用 CAS 来保证原子性。


需要注意的是,当调用 tryRelease 方法时,应该保证当前线程是持有锁的线程,否则会抛出 IllegalMonitorStateException 异常。这是为了确保只有拥有锁的线程才能释放锁,防止误释放其他线程的锁。

四、参考资料

  1. 《并发编程之美》

  2. AbstractQueuedSynchronizer抽象类的源码


本文原创,才疏学浅,如有纰漏,欢迎指正。尊贵的朋友,如果本文对您有所帮助,欢迎点赞,并期待您的反馈,以便于不断优化。

原文地址: https://www.emanjusaka.top/archives/8

微信公众号:emanjusaka 的编程栈

用户头像

emanjusaka

关注

还未添加个人签名 2022-04-29 加入

分享技术,共同成长。一个编程菜鸟的升级打怪之路~

评论

发布
暂无评论
探索抽象同步队列 AQS_Java_emanjusaka_InfoQ写作社区