前言
原文地址Java并发之AQS详解 - waterystone - 博客园 (cnblogs.com)
根据这篇文章摘取了部分进行完善,加入自己理解
AQS是什么?
AQS全称AbstractQueueSynchronizer,翻译为:抽象队列同步器,它的作用主要用来构造锁和同步器。ReentranLock与Semaphore都是基于AQS实现。
AQS原理
AQS的核心思想是如果被请求的共享资源空闲,那么将当前请求资源的线程设置为有效的工作线程,并且为共享资源设置为锁定状态。如果被请求的共享资源被占,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制。这个机制AQS是用CLH队列锁(CLH是一个虚拟的双向队列,只存在节点Node之间的关联关系)实现的,即将暂时获取不到锁的线程加入到队列当中。一个Node就表示一个线程,保存了线程引用,当前节点在队列中的状态,前驱节点,后继节点。
CLH队列结构如下所示(两者是同一个东西)
同时AQS还维护一个volatile int state变量用来表示资源的同步状态。空闲资源的state为0,当有线程执行lock()方法时state++,其他线程再去执行lock方法发现state不为0并且当前加锁的线程不是自己时就会阻塞。由于ReentrantLock是可重入锁,因此同一线程对同一资源多次lock时,state也会进行多次自增,释放锁时state--
AQS从资源访问方式可以划分为两种:独占(一个线程访问)和共享(多线程可以同时执行)
AQS源码分析
CLH队列锁AQS已经帮我们实现好了,在查看AQS的实现代码之前,我们先了解Node中维护的节点状态(waitStatus)的值都代表什么意思
- 1:表示当前节点已经取消调度,timeout或是被中断,进入该状态的节点不会再变化
- -1:表示后继节点在等待当前节点唤醒,后句节点入队后,就将前继节点状态修改为-1。
- -2:表示节点等待在Condition上,当其他线程调用Condition的signal方法后,该状态的节点从等待队列转移到同步队列,等待获取同步锁。
- -3:共享模式下前驱节点不仅会唤醒后继节点,同时也会唤醒后继的后继节点。
- 0:新节点入队时的默认状态
接下来我们就开始分析实现源码,以ReentrantLock的非公平锁为例。
final void lock() {
// CAS将state从0设置为1
if (compareAndSetState(0, 1))
// 设置资源的持有线程为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果加锁失败,那么执行acquire
acquire(1);
}
if块中的代码很好理解,接下来去查看acquire方法做了什么
public final void acquire(int arg) {
// 尝试去加锁,具体加锁逻辑由不同的同步器去实现
if (!tryAcquire(arg) &&
// 尝试去加入等待队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- tryAcquire():尝试再次获取锁,如果成功直接返回,这也是非公平锁的体现,每次新加入的线程在加入队列前都有一次再次获取锁的机会。
- addWaiter()将线程加入等待队列的队尾,并标记为独占模式
- acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回,期间如果被打断则返回ture
需要注意的是,如果线程在等待期间被中断是不会有任何影响的,当线程获取到资源后发现自己被打断才会去执行selfInterrupt方法。
private Node addWaiter(Node mode) {
// 将当前线程构造为Node对象,mode有两种值:独占与共享
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果尾节点不为空则直接将当前节点添加到尾节点之后
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果不存在尾节点或是将当前节点设为尾节点失败
enq(node);
return node;
}
private Node enq(final Node node) {
// 自选的方式来保证在多线程下节点一定添加成功
for (;;) {
Node t = tail;
// 如果是尾节点为空的情况。这种情况将当前节点设置为头节点即可
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else { // 如果是设置头节点失败或是设置尾节点失败处理逻辑
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
以上两个方式就是将未获得锁的线程放入等待队列尾部的实现代码,放入队列尾部之后,我们应该让线程进入等待状态,等待其他线程唤醒自己,具体实现代码如下
final boolean acquireQueued(final Node node, int arg) {
// 标记是否拿到锁
boolean failed = true;
try {
// 被打断标致
boolean interrupted = false;
for (;;) {
// 拿到前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头节点的话,那么去尝试获取锁看头节点是否已经释放了锁
if (p == head && tryAcquire(arg)) {
// 获取到锁,将自己设为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果没有拿到资源,那么就应该执行park进入等待状态
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;// 如果被打断则修改打断标志
}
} finally {
// 如果没有获取到资源,则通过该方法修改线程状态以及修改队列连接状态
if (failed)
cancelAcquire(node);
}
}
接下来去查看线程如何进入等待状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 得到前驱节点的线程状态
int ws = pred.waitStatus;
// 如果前驱节点状态为-1,则直接返回
if (ws == Node.SIGNAL)
return true;
// 前驱节点只要大于0说明都不会再参与资源获取,自然没有唤醒下一个线程的义务
if (ws > 0) {
// 循环获取前面节点线程状态小于0的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 将获取的节点后驱节点设置为当前节点
pred.next = node;
} else {
// 如果前驱节点为0,那么将0设置为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 执行park进行等待
LockSupport.park(this);
// 如果被唤醒则判断自己是否被打断
return Thread.interrupted();
}
经过这两个方法,只要线程不被打断那么通过自旋一定会获取到资源。
private void cancelAcquire(Node node) {
if (node == null)
return;
// 清除node信息,代表当前节点无效
node.thread = null;
Node pred = node.prev;
// 找到前驱节点中第一个等待状态还有效的
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 获取有效前驱的后继节点(已经无效了),后续需要修改有效前驱的后续节点为null
Node predNext = pred.next;
// 修改当前节点的等待状态为打断或超时
node.waitStatus = Node.CANCELLED;
// 如果当前节点为尾节点,那么将前驱节点设为尾节点
if (node == tail && compareAndSetTail(node, pred)) {
// 清空无效节点
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 如果有效前驱节点不是头节点,有唤醒后继节点的责任,那么和有效后继节点连接起来
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
// 自己指向自己,没有其他Node引用自己,等待被回收即可
node.next = node; // help GC
}
}
获取锁的流程讲解完毕接下来看释放锁的流程
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 找到头结点
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒等待队列里的下一个线程
return true;
}
return false;
}
与tryAcquire相似的是,tryRelease的实现由子类同步器去实现。只需要注意的是tryRelease方法返回true的情况只有state为0的时,只要state不为0说明线程还没有完全释放锁。
private void unparkSuccessor(Node node) {
// node一般为当前线程所在的结点。
int ws = node.waitStatus;
// 如果有唤醒下一个线程的义务
if (ws < 0)
//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
//找到后继结点s
Node s = node.next;
//如果为空或已取消
if (s == null || s.waitStatus > 0) {
s = null;
// 从后向前找,找到下一个需要被唤醒的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
我们可能会好奇为什么从后先前找,明明从前往后找下一个节点是最快速的,那么我们来去看将节点添加到队列尾部的源码
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
// 将新节点设置为tail成功,但是此时CPU时间片耗尽,
// 正好换到了释放锁的线程,那么从前往后找就会出现null的情况
pred.next = node;
return node;
}
}
enq(node);
return node;
}