万字解析AQS源码

markdown 复制代码
                  学而不思则罔,思而不学则殆
                                           ---------《论语・为政》

1.初识AQS

AQS(AbstractQueuedSynchronizer)是 Java 并发包中很多同步组件的基础,想必大家很熟悉CountDownLatch(线程栏珊) ,ReentrantLock(可重入锁),Semaphore(信号量)这三个JUC包下常用的管理并发线程的组件,而AQS是它们内部类Sync的共同父类,本文就详细拆解AQS源码和工作原理,帮助我们深入理解并发实战。

2.AQS结构和原理

折叠Node类,AQS结构实际非常简单,state字段管理状态,而Node内部类管理并发线程,下面我们分开拆解这两者

2.1 state变量

先来看下官方注解,直接翻译是同步状态,并且由volatile关键字修饰,多个线程可能存在读取不一致情况

arduino 复制代码
/**
 * The synchronization state.
 */
private volatile int state;

volatile凭借内存可见性,让写指令即时刷新到主内存,同时读操作会强制从主内存中读取,从这里我们可以隐隐想到,多个线程在并发操作时会围绕state状态来做下一步动作

2.2 Node内部类

先来看看Node内部类中的成员变量有哪些

java 复制代码
/** 用于标记一个节点正以共享模式等待 */
static final Node SHARED = new Node();
/** 用于标记一个节点正以独占模式等待 */
static final Node EXCLUSIVE = null;

/** waitStatus 值,表示线程已取消 */
static final int CANCELLED =  1;
/** waitStatus 值,表示后继节点的线程需要被唤醒(通过 unpark) */
static final int SIGNAL    = -1;
/** waitStatus 值,表示线程正在条件队列上等待 */
static final int CONDITION = -2;
/**
 * waitStatus 值,表示下一次共享式获取操作应该无条件传播
 */
static final int PROPAGATE = -3;

/**
 * 状态字段,取值仅为以下几种:
 *   SIGNAL:     此节点的后继节点(或即将)被阻塞(通过 park),因此当前节点在释放或取消时必须唤醒其后继节点。
 *               为避免竞争,获取方法必须先表明它们需要信号,然后重试原子性获取操作,失败后再阻塞。
 *   CANCELLED:  由于超时或中断,此节点已被取消。节点永远不会脱离此状态。
 *               特别地,具有已取消节点的线程永远不会再次阻塞。
 *   CONDITION:  此节点当前位于条件队列上。在转移之前,它不会用作同步队列节点,
 *               转移时状态将被设置为 0。(这里使用此值与该字段的其他用途无关,但简化了机制。)
 *   PROPAGATE:  共享式释放操作应该传播到其他节点。此状态仅在 doReleaseShared 方法中为头节点设置,
 *               以确保即使在其他操作介入的情况下,传播也能继续。
 *   0:          以上情况都不是
 *
 * 这些值按数值排列是为了简化使用。非负值意味着节点不需要发出信号。
 * 因此,大多数代码不需要检查特定的值,只需检查符号即可。
 *
 * 对于普通的同步节点,该字段初始化为 0;对于条件节点,初始化为 CONDITION。
 * 它使用 CAS(或在可能的情况下,使用无条件的 volatile 写操作)进行修改。
 */
volatile int waitStatus;

/**
 * 指向当前节点/线程依赖的前驱节点的引用,用于检查 waitStatus。
 * 在入队时赋值,仅在出队时为了垃圾回收而置为 null。
 * 此外,当前驱节点取消时,我们会在查找未取消的节点时进行短路操作,
 * 因为头节点永远不会被取消:一个节点只有在成功获取锁后才会成为头节点。
 * 一个已取消的线程永远不会成功获取锁,并且一个线程只会取消自身,不会取消其他节点。
 */
volatile Node prev;

/**
 * 指向当前节点/线程在释放时要唤醒的后继节点的引用。
 * 在入队时赋值,在绕过已取消的前驱节点时进行调整,在出队时为了垃圾回收而置为 null。
 * 入队操作在附加节点之后才会给前驱节点的 next 字段赋值,
 * 所以看到 next 字段为 null 并不一定意味着该节点是队列的末尾。
 * 然而,如果 next 字段看起来为 null,我们可以从尾部扫描前驱节点来进行双重检查。
 * 已取消节点的 next 字段会指向节点本身而不是 null,这是为了方便 isOnSyncQueue 方法的实现。
 */
volatile Node next;

/**
 * 入队此节点的线程。在构造时初始化,使用后置为 null。
 */
volatile Thread thread;

/**
 * 指向在条件队列上等待的下一个节点的引用,或者是特殊值 SHARED。
 * 因为条件队列仅在以独占模式持有锁时才会被访问,所以我们只需要一个简单的链表队列来持有等待条件的节点。
 * 然后这些节点会被转移到同步队列中重新获取锁。
 * 并且由于条件只能是独占的,我们通过使用特殊值来表示共享模式,从而节省了一个字段。
 */
Node nextWaiter;

成员还是很多的我们逐个拆解来看看

  • Node SHARED: 一个Node节点,用于标记一个节点正以共享模式等待,后面会详解共享模式
  • Node EXCLUSIVE :类似于前者,标记节点是独占模式
  • int CANCELLED;表示线程被取消
  • int SIGNAL:表示线程需要被唤醒
  • int CONDITION:表示线程处于等待队列上,后续会解释等待队列
  • int PROPAGATE:表示下次共享状态的传播逻辑
  • volatile int waitStatus:这里也是用volatile字段修饰,代表线程当前等待状态
  • volatile Node prev:前置节点
  • volatile Node next:后置节点
  • volatile Thread thread:入队的线程,在初始化的时候加入
  • Node nextWaiter:表示等待队列的下一个引用

2.3 AQS原理

了解完结构,这里画一张图帮助大家理解

AQS的原理并不复杂,AQS维护了一个volatile int state变量和一个CLH(三个人名缩写)双向队列,队列中的节点持有线程引用,每个节点均可通过getState()setState()compareAndSetState()对state进行修改和访问。

当线程获取锁时,即试图对state变量做修改,如修改成功则获取锁;如修改失败则包装为节点挂载到队列中,等待持有锁的线程释放锁并唤醒队列中的节点。

3. 队列转换逻辑

由上文结构我们可以推导出AQS内部FIFO队列结构,CLH队列和等待队列

  • 同步队列:也称为 CLH 队列,是一个 FIFO(先进先出)的双向队列,用于存储等待获取同步状态的线程节点。当一个线程尝试获取同步状态失败时,会被封装成节点加入到同步队列的尾部,并进入阻塞状态,直到被唤醒。
  • 条件队列 :是一个单向链表,用于存储调用 Condition 对象的 await() 方法后进入等待状态的线程节点。当线程调用 await() 方法时,会释放当前持有的同步状态,并将自己封装成节点加入到条件队列中;当其他线程调用 Condition 对象的 signal()signalAll() 方法时,会将条件队列中的节点转移到同步队列中,等待重新获取同步状态。

3.1从同步队列到条件队列(await() 方法)

当线程调用 Condition 对象的 await() 方法时,会触发从同步队列到条件队列的转换。

java 复制代码
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 创建一个新的条件节点并加入到条件队列
    Node node = addConditionWaiter();
    // 释放当前持有的同步状态
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 检查节点是否在同步队列中,如果不在则阻塞
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 重新获取同步状态
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
  • addConditionWaiter() :创建一个新的条件节点,并将其加入到条件队列的尾部。
  • fullyRelease(Node node) :释放当前线程持有的同步状态,并返回释放前的状态值。
  • isOnSyncQueue(Node node) :检查节点是否在同步队列中。如果不在,则线程进入阻塞状态,直到被唤醒。

3.2 从条件队列到同步队列(signal()signalAll() 方法)

signal() 方法:将条件队列中的第一个节点转移到同步队列中。

java 复制代码
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
    // 尝试将节点的等待状态从 CONDITION 改为 0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 将节点加入到同步队列中
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 ||!compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}
  • doSignal(Node first) :从条件队列的头部开始,尝试将节点转移到同步队列中,直到成功转移一个节点或条件队列为空。
  • transferForSignal(Node node) :将节点的等待状态从 CONDITION 改为 0,并将节点加入到同步队列的尾部。如果节点的前驱节点状态为 CANCELLED 或设置 SIGNAL 状态失败,则唤醒该节点对应的线程。
  • signalAll() 方法:将条件队列中的所有节点依次转移到同步队列中。
java 复制代码
public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}
  • doSignalAll(Node first) :遍历条件队列中的所有节点,将每个节点依次转移到同步队列中。

3.3 转换变量

上文Node节点有两个变量在队列转换中起到重要作用

Node nextWaiter

  • NodeCLH队列时,nextWaiter表示共享式或独占式标记
  • Node在条件队列时,nextWaiter表示下个Node节点指针

volatile int waitStatus:这里用图片表示一下

4.独占模式和共享模式

  • 独占模式 :同一时刻只允许一个线程获取同步状态并执行临界区代码,其他线程需要等待该线程释放同步状态后才有机会获取。例如,ReentrantLock 就是基于 AQS 的独占模式实现的,在同一时刻只有一个线程可以持有锁并执行被锁保护的代码块。

  • 共享模式 :同一时刻可以有多个线程同时获取同步状态并执行临界区代码。例如,Semaphore(信号量)和 CountDownLatch 就是基于 AQS 的共享模式实现的,允许多个线程同时访问有限的资源。

4.1 独占模式

独占模式底层有部分方法需要自己实现,因为ReentrantLock底层调用的AQS是独占模式,所以下文讲解的AQS源码也是针对独占模式的操作,ReentrantLock的加锁和解锁方法分别为lock()和unLock()

4.1.1 获取锁

  • lock() 方法 :这里是加锁的源头方法,逻辑很简单,线程进来后直接利用CAS尝试抢占锁,如果抢占成功state值回被改为1,且设置对象独占锁线程为当前线程,否则就调用acquire(1)再次尝试获取锁。
java 复制代码
final void lock() {
 if (compareAndSetState(0, 1))
  //设置持有锁线程
  setExclusiveOwnerThread(Thread.currentThread());
 else
  acquire(1);
}
  • 继续执行 acquire(int arg) 方法:这是独占模式下获取同步状态的入口方法。
java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire包含了几个函数的调用,

  • tryAcquire(arg):用于尝试获取同步状态。若获取成功则返回 true,失败则返回 false
  • addWaiter(Node.EXCLUSIVE):若 tryAcquire 失败,会将当前线程封装成一个独占模式的节点(Node.EXCLUSIVE),并添加到同步队列的尾部。
  • acquireQueued:使节点在队列中自旋等待获取同步状态,若期间被中断,会返回 true
  • selfInterrupt():若 acquireQueued 返回 true,则当前线程会自我中断。

看下自定义的tryAcquire(arg)方法

java 复制代码
protected final boolean tryAcquire(int acquires) {
 return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
 final Thread current = Thread.currentThread();
 int c = getState();
 if (c == 0) {
  if (compareAndSetState(0, acquires)) {
   setExclusiveOwnerThread(current);
   return true;
  }
 }
 else if (current == getExclusiveOwnerThread()) {
  int nextc = c + acquires;
  if (nextc < 0) // overflow
   throw new Error("Maximum lock count exceeded");
  setState(nextc);
  return true;
 }
 return false;
}

nonfairTryAcquire方法首先会获取state的值,如果为0,则正常获取该锁,不为0的话判断是否是当前线程占用了,是的话就累加state的值,这里的累加也是为了配合释放锁时候的次数,从而实现可重入锁的效果。

4.1.2 释放锁

  • release(int arg) 方法:用于独占模式下释放同步状态。
java 复制代码
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  • tryRelease(arg):代码上可以看出,核心的逻辑都在tryRelease方法中,该方法的作用是释放资源,AQS里该方法没有具体的实现,需要由自定义的同步器去实现,我们看下ReentrantLock代码中对应方法的源码:
java 复制代码
protected final boolean tryRelease(int releases) {
 int c = getState() - releases;
 if (Thread.currentThread() != getExclusiveOwnerThread())
  throw new IllegalMonitorStateException();
 boolean free = false;
 if (c == 0) {
  free = true;
  setExclusiveOwnerThread(null);
 }
 setState(c);
 return free;
}

tryRelease方法会减去state对应的值,如果state为0,也就是已经彻底释放资源,就返回true,并且把独占的线程置为null,否则返回false。

  • unparkSuccessor(h):若 tryRelease 成功,会唤醒同步队列中头节点的后继节点。
java 复制代码
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
     //将head结点的状态置为0
        compareAndSetWaitStatus(node, ws, 0);
 //找到下一个需要唤醒的结点s
    Node s = node.next;
    //如果为空或已取消
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从后向前,直到找到等待状态小于0的结点,前面说了,结点waitStatus小于0时才有效
        for (Node t = tail; t != null && t != node; t = t.prev) 
            if (t.waitStatus <= 0)
                s = t;
    }
    // 找到有效的结点,直接唤醒
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

方法的逻辑很简单,就是先将head的结点状态置为0,避免下面找结点的时候再找到head,然后找到队列中最前面的有效结点,然后唤醒,我们假设这个时候线程A已经释放锁,那么此时队列中排最前边竞争锁的线程B就会被唤醒。然后被唤醒的线程B就会尝试用CAS获取锁,回到acquireQueued方法的逻辑

4.2 共享模式

4.2.1 获取锁

共享模式获取锁的顶层入口方法是acquireShared,该方法会获取指定数量的资源,成功的话就直接返回,失败的话就进入等待队列,直到获取资源。

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

该方法里包含了两个方法的调用,

tryAcquireShared:ryAcquireShared在AQS里没有实现,同样由自定义的同步器去完成具体的逻辑,像一些较为常见的并发工具Semaphore、CountDownLatch里就有对该方法的自定义实现,虽然实现的逻辑不同,但方法的作用是一样的,就是获取一定资源的资源,然后根据返回值判断是否还有剩余资源,从而决定下一步的操作。 这里以CountDownLatch为例

java 复制代码
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

返回值有三种定义:

  1. 负值代表获取失败;
  2. 0代表获取成功,但没有剩余的资源,也就是state已经为0;
  3. 正值代表获取成功,而且state还有剩余,其他线程可以继续领取

当返回值小于0时,证明此次获取一定数量的锁失败了,然后就会走doAcquireShared方法

doAcquireShared:进入等待队列,并循环尝试获取锁,直到成功。

java 复制代码
private void doAcquireShared(int arg) {
 // 加入队列尾部
 final Node node = addWaiter(Node.SHARED);
 boolean failed = true;
 try {
  boolean interrupted = false;
  // CAS自旋
  for (;;) {
   final Node p = node.predecessor();
   // 判断前驱结点是否是head
   if (p == head) {
    // 尝试获取一定数量的锁
    int r = tryAcquireShared(arg);
    if (r >= 0) {
     // 获取锁成功,而且还有剩余资源,就设置当前结点为head,并继续唤醒下一个线程
     setHeadAndPropagate(node, r);
     // 让前驱结点去掉引用链,方便被GC
     p.next = null; // help GC
     if (interrupted)
      selfInterrupt();
     failed = false;
     return;
    }
   }
   // 跟独占模式一样,改前驱结点waitStatus为-1,并且当前线程挂起,等待被唤醒
   if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    interrupted = true;
  }
 } finally {
  if (failed)
   cancelAcquire(node);
 }
}

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    // head指向自己
    setHead(node);
     // 如果还有剩余量,继续唤醒下一个邻居线程
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

其实两个流程并没有太大的差别,只是doAcquireShared()比起独占模式下的获取锁上多了一步唤醒后继线程的操作,当获取完一定的资源后,发现还有剩余的资源,就继续唤醒下一个邻居线程,这才符合"共享"的思想

4.2.2 释放锁

共享模式释放锁的顶层方法是releaseShared,它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源

java 复制代码
public final boolean releaseShared(int arg) {
 if (tryReleaseShared(arg)) {
  doReleaseShared();
  return true;
 }
 return false;
}
  • tryReleaseShared:需子类实现,用于尝试共享式地释放同步状态。若释放成功则返回 true,失败则返回 false
java 复制代码
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
  • doAcquireShared:若 tryReleaseShared 成功,会唤醒同步队列中等待的线程,以保证共享状态的传播。
java 复制代码
private void doReleaseShared() {
 for (;;) {
  // 获取等待队列中的head结点
  Node h = head;
  if (h != null && h != tail) {
   int ws = h.waitStatus;
   // head结点waitStatus = -1,唤醒下一个结点对应的线程
   if (ws == Node.SIGNAL) {
    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
     continue;            // loop to recheck cases
    // 唤醒后继结点
    unparkSuccessor(h);
   }
   else if (ws == 0 &&
      !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
    continue;                // loop on failed CAS
  }
  if (h == head)                   // loop if head changed
   break;
 }
}

如果等待队列head结点的waitStatus为-1的话,就直接唤醒后继结点,唤醒的方法unparkSuccessor()

相关推荐
AI小智1 分钟前
AI提效99.5%!英国政府联手 Gemini,破解城市规划审批困局
后端
风象南1 分钟前
SpringBoot的4种抽奖活动实现策略
java·spring boot·后端
运维成长记5 分钟前
Zabbix 高可用架构部署方案(2最新版)
mysql·架构·zabbix
why1517 小时前
微服务商城-商品微服务
数据库·后端·golang
結城9 小时前
mybatisX的使用,简化springboot的开发,不用再写entity、mapper以及service了!
java·spring boot·后端
星辰离彬9 小时前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
java·spring boot·后端·sql·mysql·性能优化
q_19132846959 小时前
基于Springboot+Vue的办公管理系统
java·vue.js·spring boot·后端·intellij idea
陪我一起学编程10 小时前
关于nvm与node.js
vue.js·后端·npm·node.js
舒一笑11 小时前
基于KubeSphere平台快速搭建单节点向量数据库Milvus
后端
JavaBuild11 小时前
时隔半年,拾笔分享:来自一个大龄程序员的迷茫自问
后端·程序员·创业