探索抽象同步队列 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 队列中的两个关键节点。
head 指针:head 指针指向 CLH 队列中的首个节点,该节点表示当前持有锁的线程。当一个线程成功地获取到锁时,它就成为了持有锁的线程,并且会将该信息记录在 head 指针所指向的节点中。
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 独占式
当一个线程调用 acquire(int arg)方法获取独占资源时,会首先使用 tryAcquire 方法尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回,失败则将当前线程封装为类型为 Node.EXCLUSIVE 的 Node 节点后插入到 AQS 阻塞队列的尾部,并调用 LockSupport.park(this)方法挂起自己。
当一个线程调用 release(int arg)方法时会尝试使用 tryRelease 操作释放资源,这里是设置状态变量 state 的值,然后调用 LockSupport.unpark(thread)方法激活 AQS 队列里面被阻塞的一个线程(thread)。被激活的线程则使用 tryAcquire 尝试,看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。
2.2 共享式
当线程调用 acquireShared(int arg)获取共享资源时,会首先使用 tryAcquireShared 尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回,失败则将当前线程封装为类型为 Node.SHARED 的 Node 节点后插入到 AQS 阻塞队列的尾部,并使用 LockSupport.park(this)方法挂起自己。
当一个线程调用 releaseShared(int arg)时会尝试使用 tryReleaseShared 操作释放资源,这里是设置状态变量 state 的值,然后使用 LockSupport.unpark(thread)激活 AQS 队列里面被阻塞的一个线程(thread)。被激活的线程则使用 tryReleaseShared 查看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。
三、基于 AQS 实现自定义同步器
基于 AQS 实现一个不可重入的独占锁,自定义 AQS 需要重写一系列函数,还需要定义原子变量 state 的含义。这里定义,state 为 0 表示目前锁没有被线程持有,state 为 1 表示锁已经被某一个线程持有,由于是不可重入锁,所以不需要记录持有锁的线程获取锁的次数。
在释放锁时并没有使用 CAS(Compare and Swap)操作,而是直接使用了 setState
方法来将 state
的值设置为 0。
这是因为在释放锁的过程中,并不需要涉及到多线程并发的问题。只有持有锁的线程才能够释放锁,其他线程无法对锁进行操作。因此,不需要使用 CAS 来进行原子性的状态更新。
在这种情况下,可以直接使用普通的方法来设置 state
的值为 0,将独占锁的拥有者设为 null。因为只有一个线程可以操作这个锁,不存在并发竞争的情况,也就不需要使用 CAS 来保证原子性。
需要注意的是,当调用 tryRelease
方法时,应该保证当前线程是持有锁的线程,否则会抛出 IllegalMonitorStateException
异常。这是为了确保只有拥有锁的线程才能释放锁,防止误释放其他线程的锁。
四、参考资料
《并发编程之美》
AbstractQueuedSynchronizer
抽象类的源码
本文原创,才疏学浅,如有纰漏,欢迎指正。尊贵的朋友,如果本文对您有所帮助,欢迎点赞,并期待您的反馈,以便于不断优化。
原文地址: https://www.emanjusaka.top/archives/8
微信公众号:emanjusaka 的编程栈
评论