ReentrantLock(重入锁)之聊聊AQS

531 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

  • 之前简单介绍了一下Lock锁-> 聊聊Lock锁,接下来看看ReentrantLock。

ReentrantLock(重入锁)之聊聊AQS

1)概览

ReentrantLock实现了Lock接口,表示重入锁。是线程在获得锁之后,不需要阻塞就可以再次获取锁,然后直接关联一次计数器增加重入次数,这就意味着一个线程能够对一个临界资源重复加锁。以下是它与Synchronized的一些对比:

ReentrantLockSynchronized
锁实现机制AQS实现监视器模式实现
灵活性支持响应中断、超时、尝试获取锁不灵活
释放形式必须显示调用unlock()进行解锁自动释放监视器
锁类型必须显示调用unlock()进行解锁自动释放监视器
条件队列可关联多个条件队列关联一个条件队列
可重入支持支持

2)什么是AQS

可以看到ReentrantLock的实现是依靠AQS来实现的,那么什么是AQS呢?

AQS全称AbstractQueuedSynchronizer,即抽象的队列同步器,是一种用来构建锁和同步器的框架。他的核心思想就是实现同步,当一个共享资源被请求时是处于空闲状态的,那么AQS就会将当前请求资源的线程设置为状态有效的线程,并且给请求到的资源加锁。而那些请求已经加了锁的资源而失败的线程或者说在刚刚争用资源时失败了的线程,则会通过一套线程阻塞等待以及被唤醒时锁分配的机制来进行管理。在AQS中是通过一个变体的 CLH 队列来实现的。

image-20220418091647330

AQS 中会将竞争共享资源失败的线程及其状态信息封装到一个node中加入到一个变体的 CLH 队列中,接下来会不断自旋(cas)尝试获取锁,条件是当前节点是头结点的直接后继才会尝试。失败一定次数后则阻塞自己等待被唤醒。而持有锁的线程释放锁时会唤醒后继的节点中的线程。

image-20220418092533237

3)什么是CLH队列

参考:juejin.cn/post/689627…

CLH:Craig、Landin and Hagersten 队列,是 单向链表实现的队列。申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现 前驱节点释放了锁就结束自旋

image-20220418092907692

CLH 队列具有以下特点:

  1. CLH 队列是一个单向链表,有着 FIFO 先进先出的队列特性
  2. 通过 tail 尾节点(原子引用)来构建队列,总是指向最后一个节点
  3. 未获得锁节点会进行自旋,而不是切换线程状态
  4. 并发高时性能较差,因为会有大量的为获得锁的线程不断轮询前驱节点的状态,会造成一定的资源浪费

AQS 中的队列是 CLH 变体的虚拟双向队列,通过将每条请求共享资源的线程封装成一个节点来实现锁的分配

image-20220418092941448

相比于 CLH 队列而言,AQS 中的 CLH 变体等待队列拥有以下特性

  1. AQS 中队列是个双向链表,也具有 FIFO 先进先出的特性
  2. 通过 Head、Tail 头尾两个节点来组成队列结构,通过 volatile 修饰保证可见性
  3. Head 指向节点为已获得锁的节点,是一个虚拟节点,节点本身不持有具体线程
  4. 获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后会将线程阻塞,相对于 CLH 队列性能较好,并发高时性能不会有太大的影响

4)AQS中node的组成

node是用来存放线程及其附带的一些信息用的,一些主要的属性如下

int waitStatus :节点状态

volatile Node prev :当前节点中的线程的前驱节点

volatile Node next :当前节点中的线程的后继节点

volatile Thread thread:当前节点中的线程

Node nextWaiter:在同步队列里用来标识节点是独占锁节点还是共享锁节点,在条件队列里代表条件条件队列的下一个节点

同时waitStatus涉及到4个可选的状态:

        /** 表示线程已取消 */
        static final int CANCELLED =  1;
        /** 表示线程等待唤醒 */
        static final int SIGNAL    = -1;
        /** 表示线程等待获取同步锁 */
        static final int CONDITION = -2;
        /** 表示共享模式下无条件传播 */
        static final int PROPAGATE = -3;

CANCELLED:代表取消状态,该线程节点已释放(超时、中断),已取消的节点不会再阻塞

SIGNAL:代表通知状态,这个状态下的节点如果被唤醒,就有义务去唤醒它的后继节点。这也就是为什么一个节点的线程阻塞之前必须保证前一个节点是 SIGNAL 状态。

CONDITION :代表条件等待状态,条件等待队列里每一个节点都是这个状态,它的节点被移到同步队列之后状态会修改为 0。

PROPAGATE:代表传播状态,在一些地方用于修复 bug 和提高性能,减少不必要的循环。

ps: 如果 waiterStatus 的值为 0,有两种情况:1、节点状态值没有被更新过(同步队列里最后一个节点的状态);2、在唤醒线程之前头节点状态会被被修改为 0。

tips: 负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。

image-20220419160915290

5)AQS的两种队列

AQS 总共有两种队列,从node的构造方式中也可以看出,一种是用于同步队列,代表的是正常的获取释放锁的队列;另外一种是条件队列,代表的是每个 ConditionObject 对应的队列。两者都是FIFO(先进先出)队列。

image-20220419161335679

同步队列

同步队列是一个双向列表,其内的节点有两种,一种是独占锁的节点,一种是共享锁的节点,两者的区别是独占的节点的nextWaiter 指向null,共享锁的nextWaiter 只想一个静态的SHARED 节点。两种队列都包括head节点和tail节点。head节点是一个空的头节点,主要用作后续的调度。

image-20220419210611913

条件队列

条件队列是单链,它没有空的头节点,每个节点都有对应的线程。条件队列头节点和尾节点的指针分别是 firstWaiter 和 lastWaiter 。

image-20220419211214815

6)Condition接口

上面说到了条件队列,条件等待和条件唤醒功能一般都是 ReentrantLock 与 AQS 的内部类 配合实现的。一个 ReentrantLock 可以创建多个 ConditionObject 实例,每个实例对应一个条件队列,以保证每个实例都有自己的等待唤醒逻辑,不会相互影响。条件队列里的线程对应的节点被唤醒时会被放到 ReentrantLock 的同步队列里,让同步队列去完成唤醒和重新尝试获取锁的工作。可以理解为条件队列是依赖同步队列的,它们协同才能完成条件等待和条件唤醒功能。

而在AQS中ConditionObject 是通过实现Condition接口来完成的,类似Object的wait()、wait(long timeout)、notify()以及notifyAll()的方法结合synchronized内置锁可以实现可以实现等待/通知模式,Condition接口定义了await()、awaitNanos(long)、signal()、signalAll()等方法,配合对象锁实例实现等待/通知功能。

image-20220419213817740