理解 AQS

AQS 全称 AbstractQueuedSynchronizer,J.U.C 并发包中的核心基础组件之一,可用来构建锁和同步器。它底层用了 CAS 技术来保证操作的原子性,同时利用 FIFO 队列管理竞争锁的线程,ReentrantLock、CountDownLatch 等同步API 都是基于AQS实现的

本文目标

通过 ReentrantLock 为入口,深入理解 AQS

AQS 数据结构

// FIFO 队列的首节点
private transient volatile Node head;

// FIFO 队列的尾节点
private transient volatile Node tail;

// 当前的同步状态
private volatile int state;

static final class Node {
    // 当前节点状态
    volatile int waitStatus;

    // 上一节点
    volatile Node prev;
    
    // 下一节点
    volatile Node next;

    // 每个Node对应一个线程
    volatile Thread thread;
    
    // 当前模式:独占/共享
    Node nextWaiter;

}

Node 组成了我们刚刚说的 FIFO

理解 AQS_第1张图片
图片来自网络

这里只是简单给大家介绍一下,AQS的数据结构,别慌。可能有些参数你并不理解,后文我们会详细解释。我第一次看AQS的源码的时候,我也很懵逼。别着急,慢慢看,万一看懂了呢

直接看AQS的源码有点生硬,我们来通过 ReentrantLock 来理解AQS

ReentrantLock 如何应用AQS

来看一下类机构

理解 AQS_第2张图片
image.png

ReentrantLock 的内部类 Sync 继承了 AQS,ReentrantLock完全依赖Sync 来实现锁。在Sync 基础上又衍生了,FairSync,NonFairSync 即公平锁和非公平锁

理解 AQS_第3张图片

默认的时候,构建的是非公平锁,我们也可以指定 fair 来决定构建 Fair 还是 Nonfair

回忆 ReentrantLock 使用方法
 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

这是JDK注释中的一个小栗子,通常来说我们用的比较多的方法,就是lock(), trylock(), unlock() 这几个。

OK. 那我们就先从 lock() 方法搞起

ReentrantLock.lock()

这里我抽出主要的代码用于演示,这里以非公平锁为例

理解 AQS_第4张图片
图1

第一步就不说了

第二步,通过compareAndSetState 尝试将 state 状态从0更新到1,这里用到就是java的 CAS(不了解CAS的同学可以Google一下)。这里ReentrantLock 利用 AQS 中的 state 变量做为锁的状态,0代表无锁,1代表当前有锁,如果CAS 更新state 成功,则说明当前线程获取锁成功,将独占线程设置为当前线程。

第三步,调用 acquire(1); 该方法是AQS 提供的,acquire 会尝试获取锁,再获取失败后,当前线程进入队列等待

这里也可以看出,NonfairSync 非常的霸道,他在调用lock方法后,就想尝试获取锁

AQS.acquire()

在第一次CAS尝试获取锁失败后,会执行到 acquire() 下面来分析一下该方法的逻辑

理解 AQS_第5张图片
图2

第一步 tryAcquire,该方法AQS 并没有提供具体实现,从图1可以看到,非公平锁的 tryAcquire() 调用的是 nonfairTryAcquire() ,具体代码在图3中

和 lock() 中的代码有点相似,首先是判断状态,如果无锁则尝试 CAS更新state,如果更新成功则代表获取锁成功。
然后 else if 这段的逻辑就是如果 “当前线程已经获取锁”,还可以继续更新state。这是啥意思?这就是为什么叫可重入锁,代表当前线程即使执行 lock(),还可以继续执行lock()

第二步,在tryAcquire 获取锁失败后,AQS 会尝试让当前线程作为一个Node入队(加入到队列的末尾),将当前线程构建为Node对象,模式为“独占”。如果CAS入队失败,或者队列为空,调用enq,自旋入队(不断的尝试CAS入队,保证线程安全)。这一步具体代码参考图2

理解 AQS_第6张图片
图3
AQS.acquireQueued()

前两次抢占锁失败的线程会进入到AQS的 FIFO 队列中,队列中的节点按照“先进先出”的规则出队,通过自旋尝试获取锁

理解 AQS_第7张图片
图4

第一步,如果当前Node 的上一个节点为head,则调用tryAcquire 尝试获得锁

这里解释一下,为什么满足 “p == head” 的Node 有资格获得锁,因为在AQS的队列中,head 节点是无意义的。为什么这么说,可以看下图2中的 enq(),初始化队列的时候,会new 一个Node 作为 head。再看图4中,如果第二个节点成功获取锁,该节点会升级为head。

第二步,shouldParkAfterFailedAcquire 从方法名我们可以分析出,该方法是判断当前线程再获取锁失败后,是否可以进入挂起状态。

在看shouldParkAfterFailedAcquire() 之前,先来了解一下 Node中waitStatus 的两个状态

/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1; // 表示当前线程被取消
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1; // 表示后续线程需要挂起

具体代码在下面图5中

  • 如果上一节点状态为 SIGNAL,返回true,当前线程会进入挂起
  • 如果上一节点状态为 CANCELLED,则将这个节点从队列中移除
  • waitStatus 为其他状态时,CAS 尝试将waitStatus 更新为 SIGNAL
理解 AQS_第8张图片
图5

第三步,如果shouldParkAfterFailedAcquire 返回true,则 parkAndCheckInterrupt 挂起当前线程

如果 acquireQueued() 方法执行过程中,如果抛出异常,入队失败,则调用cancelAcquire() 取消当前节点

总结

上面我们分析了 ReentrantLock 的部分源码,ReentrantLock 通过AQS提供的state 以及 FIFO,巧妙的实现了锁。其实AQS的源码远不止这些,还有很多值得思考的东西,后续还会为大家分享相关的知识。

最后我们通过一张图,再来加深一下本节的理解

理解 AQS_第9张图片

如果觉得还不错欢迎点赞、转发。你的支持就是对我最大的帮助

你可能感兴趣的:(理解 AQS)