天天看点

面试必考AQS-AQS源码全局分析

源码划分

在《protected boolean tryAcquire(int arg); // 尝试获取排他锁

protected boolean tryRelease(int arg); // 尝试释放排他锁

protected int tryAcquireShared(int arg); // 尝试获取共享锁

protected boolean tryReleaseShared(int arg); // 尝试释放共享锁

protected boolean isHeldExclusively(); // 判断当前线程是否持有排他锁

由这5个方法可知,AQS中涉及了排它锁和共享锁两类的操作,每种锁又包含申请和释放两类操作流程,每个流程中又涉及到多个实例方法和抽象方法调用。

在AQS中,方法分为可中断的、不可中断的、同步的、带超时时间的同步的,目前整理的流程是“不可中断的同步的”。

实例方法介绍

排他锁的获取流程

排它锁,又称独占锁,同一时刻只能有一个线程申请到,其他线程进入同步队列,等待锁释放后,按队列顺序申请排它锁;涉及到的方法及功能依次为:

public final void acquire(int arg){...} // 获取排它锁的入口
# protected boolean tryAcquire(int arg); // 尝试直接获取锁
final boolean acquireQueued(final Node node, int arg) {...} // AQS中获取排它锁流程整合
private Node addWaiter(Node mode){...} // 将node加入到同步队列的尾部
# protected boolean tryAcquire(int arg); // 如果当前node的前置节点pre变为了head节点,则尝试获取锁(因为head可能正在释放)
private void setHead(Node node) {...} // 设置 同步队列的head节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {...} // 如果获取锁失败,则整理队中节点状态,并判断是否要将线程挂起
private final boolean parkAndCheckInterrupt() {...} // 将线程挂起,并在挂起被唤醒后检查是否要中断线程(返回是否中断)
private void cancelAcquire(Node node) {...} // 取消当前节点获取排它锁,将其从同步队列中移除
static void selfInterrupt() {...} // 操作将当前线程中断
           

看上面的流程中,涉及到2次抽象方法tryAcquire(int arg)的调用,也就是在申请过程中需要实现类的参与,它决定了达到何种状态才算申请成功。

排他锁的释放流程

释放的调用流程较为简单,涉及到的方法及功能依次为:

public final boolean release(int arg) {...} // 释放排它锁的入口
# protected boolean tryRelease(int arg) ; // 尝试直接释放锁
private void unparkSuccessor(Node node) {...} // 唤醒后继节点
           

共享锁的获取流程

共享锁,允许同一时刻有多个线程持有,但共享锁的申请会考虑当前排它锁的状态,也就是当存在排它锁时,共享锁的申请也要进入同步队列等待,直到排它锁被释放,才真正去申请;涉及到的方法及功能依次为:

public final void acquireShared(int arg) {...} // 获取共享锁的入口
# protected int tryAcquireShared(int arg); // 尝试获取共享锁
private void doAcquireShared(int arg) {...} // AQS中获取共享锁流程整合
private Node addWaiter(Node mode){...} // 将node加入到同步队列的尾部
# protected int tryAcquireShared(int arg); // 尝试获取共享锁
private void setHeadAndPropagate(Node node, int propagate) {...} // 设置 同步队列的head节点,以及触发"传播"操作
# private void doReleaseShared() {...} // 遍历同步队列,调整节点状态,唤醒待申请节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {...} // 如果获取锁失败,则整理队中节点状态,并判断是否要将线程挂起 
private final boolean parkAndCheckInterrupt() {...} // 将线程挂起,并在挂起被唤醒后检查是否要中断线程(返回是否中断)
private void cancelAcquire(Node node) {...} // 取消当前节点获取排它锁,将其从同步队列中移除
           

共享锁的释放流程

释放的调用流程较为简单,涉及到的方法及功能依次为:

public final boolean releaseShared(int arg) {...} // 释放共享锁的入口
# protected boolean tryReleaseShared(int arg); // 尝试释放共享锁
private void doReleaseShared() {...} // 遍历同步队列,调整节点状态,唤醒待申请节点
           

在细致读完AQS中申请和释放锁的简略流程后,会发现里面涉及到大量针对Node和同步队列的处理。这就涉及到CLH模型了,该模型的介绍可阅读《// 初始化head节点或shared节点使用

Node() {

}

// CLH模型的同步队里中使用

Node(Thread thread, Node mode) {

this.nextWaiter = mode;

this.thread = thread;

}

// Condition的等待队列中使用

Node(Thread thread, int waitStatus) {

this.waitStatus = waitStatus;

this.thread = thread;

}

三个构造函数,根据注释可知,在锁操作期间涉及到的有2个,分别是无参构造函数Node()和包含线程及节点的Node(Thread thread, Node mode)。在实际使用过程中,Node mode入参使用的是Node SHARED和Node EXCLUSIVE两个对象。

常量字段

static final Node SHARED = new Node(); // 标识共享锁节点
static final Node EXCLUSIVE = null; // 标识排它锁节点
// 以下常量,都是waitStatus字段可能的值
static final int CANCELLED =  1; // 取消状态,也就是不想申请或释放了
static final int SIGNAL    = -1; // 等待通知状态,当节点变为这个状态,说明后继节点等待被唤醒
static final int CONDITION = -2; // 条件状态,await/signal 场景下使用
static final int PROPAGATE = -3; // 传播状态,这个很diao,需要长篇幅的介绍思路
           

四个int类型的常量,一定要牢记,后面的流程中会经常涉及到waitStatus值的判断,在这里可以先划分一下类型,值小于0的都可以认为值等待状态(不同的值涉及到不同的情况及用法),而大于0的则是无效状态,需要将节点从同步队列中处理掉。

变量字段

volatile int waitStatus; // 用于标识节点状态
volatile Node prev; // 前置节点指针
volatile Node next; // 后继节点指针
volatile Thread thread; // 持有的线程对象,可以理解为每个node代表一个线程
Node nextWaiter; // 等待队列下一个节点指针(Condition中使用)
           

这里的waitStatus则是当前节点的具体状态;prev和next则是两个指针,用于快速操作前后节点;thread则是node对象持有的线程对象,也就是当前是哪个线程准备申请/释放锁;至于nextWaiter是在Condition中使用的,这个将来介绍await和signal的时候再做介绍。

方法

// 判断是否为申请共享锁的节点;
final boolean isShared() {
	return nextWaiter == SHARED;
}

// 获取前置节点,如果为null抛出异常
final Node predecessor() throws NullPointerException {
	Node p = prev;
	if (p == null)
		throw new NullPointerException();
	else
		return p;
}
           

关于Node类的实例方法,只有2个,一个是判断当前节点是否为共享类型的节点,另一个是获取当前节点的前置节点对象。

以上就是Node类的所有成员,在申请和释放锁的流程流程中涉及到大量对它们的操作。

AQS成员变量

在AQS当中,申请锁过程中除了CAS相关成员外,核心的成员变量就三个:

private transient volatile Node head;
private transient volatile Node tail;
private volatile int state; // 同步器状态,它的值决定了是否持有锁
           

其中head和tail就是同步队列的头尾,对于head,就是当前申请到锁的节点,而tail则是指向新加入同步队列的节点。在每次申请到锁时会调整head的指向,释放锁时会将head指向下一个节点。而新节点入队则是采用尾插方式,并且调整tail节点指向新插入的node对象。

总结

本文主要是对AQS的源码做了一次比较笼统的概念,是基于对AQS有一定熟悉程度的前提下描述的,建议读者在未来阅读完本系列后,回到这篇文章,对AQS的整体再加深一次理解。后续将会对AQS中锁的申请与释放详细分析其源码逻辑。