写点什么

一文读懂 AQS 的前世今生

  • 2023-12-06
    湖南
  • 本文字数:1769 字

    阅读完需:约 6 分钟

一文读懂AQS的前世今生

一、AQS 是什么?

AQS 本质就是 JUC 包下的一个抽象类 AbstractQueuedSynchronizer。AQS 本身不提供并发下的一个具体的功能,都是 JUC 包的一些工具类,锁之类的内容,需要基于 AQS 去实现。

更多的 AQS 是作为一个基础类,让其他的类去继承。

AQS 中大致有三个核心内容

  • state 属性: 本质就是一个 int 类型,但是这个属性及其重要。比如 ReentrantLock,如果 state 为 0,代表没有线程持有当前锁资源。如果 state >0 代表某个线程在持有当前锁资源。

private volatile int state;
复制代码
  • 同步队列(双向链表):一个由 Node 对象组成的同步队列。如果某个线程获取锁资源失败了,当前线程需要排队等一会,这个同步队列就是排队等待的地儿。

双向链表

  • 单向链表: 也是由 Node 对象组成的一个单向链表。比如持有锁的线程,执行了 await 方法,当前线程会封装为 Node,添加到这个单向链表中。

单向链表

二、AQS 核心底层和 Lock 是什么关系?

ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解 ReentrantLock 的特性,我们先将 ReentrantLock 跟常用的 Synchronized 进行比较,其特性如下:

ReentrantLock 就是借助了 AQS 实现的锁机制。只不过 ReentrantLock 类没有直接去的继承 AQS,而是他的内部类 Sync 去继承的 AQS,实现的一些具体的业务逻辑。

锁的一些逻辑是由抽象类 Sync 实现的,一些特有的功能,比如和公平以及非公平锁先关的内容是 Sync 的子类

  • NonfairSync: 非公平锁

  • FairSync: 公平锁

使用 ReentrantLock 可以指定是公平锁不是非公平锁,可以通过 ReentrantLock 提供的有参构造来决定使用哪种方式,默认情况下,使用的是非公平锁。如果参数传递为 true,采用公平锁。

优先分析一下 lock 方法的逻辑。

发现 ReentrantLock 执行 lock 方法后,会直接调用 sync 的 lock 方法。但是 sync 没有直接实现 lock 方法的逻辑,是由 NonfairSync 和 FairSync 去实现的具体逻辑。

先看 NonfairSync:

加锁总结:

  • 通过 ReentrantLock 的加锁方法 Lock 进行加锁操作。

  • 会调用到内部类 Sync 的 Lock 方法,由于 Sync#lock 是抽象方法,根据 ReentrantLock 初始化选择的公平锁和非公平锁,执行相关内部类的 Lock 方法,本质上都会执行 AQS 的 Acquire 方法。

  • AQS 的 Acquire 方法会执行 tryAcquire 方法,但是由于 tryAcquire 需要自定义同步器实现,因此执行了 ReentrantLock 中的 tryAcquire 方法,由于 ReentrantLock 是通过公平锁和非公平锁内部类实现的 tryAcquire 方法,因此会根据锁类型不同,执行不同的 tryAcquire。

  • tryAcquire 是获取锁逻辑,获取失败后,会执行框架 AQS 的后续逻辑,跟 ReentrantLock 自定义同步器无关。

解锁总结:

  • 通过 ReentrantLock 的解锁方法 Unlock 进行解锁。

  • Unlock 会调用内部类 Sync 的 Release 方法,该方法继承于 AQS。

  • Release 中会调用 tryRelease 方法,tryRelease 需要自定义同步器实现,tryRelease 只在 ReentrantLock 中的 Sync 实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。

  • 释放成功后,所有处理由 AQS 框架完成,与自定义同步器无关。

三、AQS 如何尝试获取资源?

发现 tryAcquire 在 AQS 中并没有具体的实现逻辑,需要子类自己去实现在 ReentrantLock 中,就提供了两种实现的机制:

非公平锁源码实现

公平锁源码实现

四、AQS 获取资源失败如何排队?

当线程基于 tryAcquire 获取锁资源失败后,需要执行 addWaiter 方法,去 AQS 的同步队列中排队。这里的逻辑确保 AQS 同步队列初始化有监控节点后,将当前节点插入到最后面~~~不存在失败的情况,死循环插不成功就一直尝试。源码如下:

如果扔到同步队列失败,基于 eng 保证可以去排队 AQS 的同步队列,在头部的节点是一个伪/监控的节点在 AQS 的同步队列还没有初始化时,先初始化一个 Node,作为监控节点然后再将当前获取锁资源失败的线程排到最后面。

主要的流程总结如下:

  • 通过当前的线程和锁模式新建一个节点。

  • Pred 指针指向尾节点 Tail。

  • 将 New 中 Node 的 Prev 指针指向 Pred。

  • 通过 compareAndSetTail 方法,完成尾节点的设置。这个方法主要是对 tailOffset 和 Expect 进行比较,如果 tailOffset 的 Node 和 Expect 的 Node 地址是相同的,那么设置 Tail 的值为 Update 的值。

五、AQS 排队后如何重新尝试获取资源?

前面获取锁资源失败了,并且已经基于 addWaiter 扔到同步队列排队之后,要走当前逻辑 acquireQueued 方法如果 Node 节点是排在 head.next 位置的,直接尝试抢锁如果没抢到,或者是不是排在 head.next 位置的。

  • 确保自己能够被唤醒

  • 挂起线程 (当释放锁后,如果 head 节点的状态是-1,就会唤醒后续节点)


发布于: 刚刚阅读数: 5
用户头像

java技术来一打~ 2023-12-01 加入

还未添加个人简介

评论

发布
暂无评论
一文读懂AQS的前世今生_Java 面试题_是月月啊2023_InfoQ写作社区