AbstractQueuedSynchronizer(以下简称AQS)作为java.util.concurrent包的基础,它提供了一套完整的同步编程框架,开发人员只需要实现其中几个简单的方法就能自由的使用诸如独占,共享,条件队列等多种同步模式。我们常用的比如ReentrantLock,CountDownLatch等等基础类库都是基于AQS实现的,足以说明这套框架的强大之处。鉴于此,我们开发人员更应该了解它的实现原理,这样才能在使用过程中得心应手。
总体来说个人感觉AQS的代码非常难懂,本文就其中的独占锁实现原理进行分析。
执行过程概述
获取锁过程:
- 首先执行acquire()方法尝试获取锁,如果成功则进入临界区
- 如果获取失败进入一个FIFO等待队列,然后被挂起等待唤醒
- 如果队列中的等待线程被唤醒会重新尝试获取锁资源,如果成功则进入临界区,否则继续挂起等待
释放锁过程:
- 使用release()尝试释放锁,如果没有其他线程在等待则释放完成
- 如果有其他线程等待则在释放锁成功后将第一个等待线程唤醒
源码深入分析
基于上面的独占锁释放过程,下面就独占锁实现原理进行分析:
同步队列
同步队列节点源码:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
......
//AQS通过头尾指针管理同步队列
private transient volatile Node head;//头节点
private transient volatile Node tail;//尾节点
private volatile int state;//同步状态
......
static final class Node {
//waitStatus的值
static final int CANCELLED = 1;//节点从同步队列中取消
static final int SIGNAL = -1;//后继节点的线程被唤醒,处于等待状态。当前节点释放同步状态后会通知后继节点,使得后继节点线程能够运行
static final int CONDITION = -2;//当前节点处于阻塞队列中,无法在同步队列中使用,直到调用signal()方法将其转移到同步队列中
static final int PROPAGATE = -3;//表示下一次共享模式下获取同步状态会无条件持续传播下去
volatile int waitStatus;//节点状态,初始状态为0
volatile Node prev;//前驱
volatile Node next;//后继
volatile Thread thread;//线程引用
......
}
......
}
通过源码,可以知道:
1.同步队列节点的数据结构,节点的状态等信息
2.AQS的同步队列是一个链式双向队列,通过头尾指针来管理同步队列。
该双向队列示意图如下:
acquire()方法(获取独占锁)
下面先贴上源码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
由源码可知,acquire()
方法它会先调用tryAcquire()
尝试获取(独占)锁,若成功则直接返回;如果获取失败,它会再调用acquireQueued()
和addWaiter()
方法将当前线程加入同步队列。
那么它是如何将线程加入同步队列的呢,下面我们研究addWaiter()
和acquireQueued()
方法。
addWaiter()
我们还是直接看addWaiter方法的源码:
//私有方法
private Node addWaiter(Node mode) {
//使用当前线程构造同步队列节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//如果同步队列尾节点不为空,则将该节点加入同步队列
if (pred != null) {
node.prev = pred;
//这一步,无论有多少个线程尝试将自己变为同步队列尾节点,最终只有一个成功,
//这一步与enq方法中的相同,只是此处只是尝试一次,而enq方法中是循环执行!
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);//如果尝试失败调用enq方法,enq方法具体实现请往下看
return node;
}
这里我们小结一下addWaiter方法中做的事情:
节点进入addWaiter方法后,如果同步队列尾节点不为空(同步队列为空的情况在enq方法里处理),会尝试一次将自己插入队列尾部(此处尝试的意义应该是为了提高性能),若成功则直接返回;失败则进入enq方法。
enq()
还是直接上源码
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//此处处理队列为空的情况。队列如果为空则CAS操作将当前线程节点设置为头结点
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
//在多线程下,此处会有多个新创建的节点指向尾节点
node.prev = t;
//队列不为空,将当前线程节点插入队列尾部(与addWaiter方法中的操作完全相同)。
//如果有多个线程都进行这个操作,最终只有一个线程插入成功,
//剩余的线程节点进入下一个循环重新"竞争"插入同步队列的机会
if (compareAndSetTail(t, node)) {
t.next = node;
//该循环体唯一退出循环的操作,否则就要无限重试
return t;
}
}
}
}
此处小结一下enq方法
以下的操作均是在循环体内!
1.处理队列为空的情况。处理操作是新建空节点作为同步队列的头结点
(此处需要注意!初始化队列触发的条件是已经有线程占有了锁资源!所以这个空节点可以看成当前占有锁资源的节点,(虽然它没有任何属性))
2.将当前线程节点插入同步队列,若成功则返回;失败则进入下一个循环
acquiredQueued()
经过上面的操作,节点已经成功插入同步队列,下面节点要做的就是挂起当前线程,等待被唤醒。这个逻辑的实现就在acquiredQueued()
方法中,下面我们先看acquiredQueued()
源码:
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
//锁被成功获取,失败标记位设为false
failed = false;
//返回中断标记位,表示节点是被正常唤醒还是被中断唤醒
return interrupted;
}
//如果获取锁失败,则进入挂起逻辑
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//最后分析获取锁失败处理逻辑
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire()
//此处先拿出waitStatus的几个取值方便下面查看
// static final int CANCELLED = 1;//节点从同步队列中取消
// static final int SIGNAL = -1;//后继节点的线程被唤醒,处于等待状态。当前节点释放同步状态后会通知后继节点,使得后继节点线程能够运行
// static final int CONDITION = -2;//当前节点处于阻塞队列中,无法在同步队列中使用,直到调用signal()方法将其转移到同步队列中
// static final int PROPAGATE = -3;//表示下一次共享模式下获取同步状态会无条件持续传播下去
//node是当前线程节点,pred是它的前驱节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前置节点的waitStatus
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前置节点的waitSatus是Node.SIGNAL则返回true,然后执行parkAndCheckInterrupt()方法进行挂起
return true;
if (ws > 0) {
//waitStatus大于0代表节点被取消
do {
//这里我们从当前节点的前置节点开始,一直向前面找第一个没有被取消的节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//注意:由于head是由new Node()创建,waitStatus为0,所以最多查到head,不会存在空指针的问题
pred.next = node;
} else {
//根据waitStatus的取值我们可知,此处waitStatus只能取得0或者Node.PROPAGATE,
//我们将前置节点的waitStatus设为Node.SIGNAL然后重新进入方法进行判断
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
上面这个方法逻辑比较复杂,它是用来判断当前节点是否可以被挂起,也就是唤醒条件是否已经具备,即如果挂起了,那一定是可以由其他线程来唤醒的。该方法如果返回false,即挂起条件没有完备,那就会重新执行acquireQueued方法的循环体,进行重新判断,如果返回true,那就表示万事俱备,可以挂起了,就会进入parkAndCheckInterrupt()方法看下源码:
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//被唤醒之后,返回中断标记,即如果是正常唤醒则返回false,如果是由于中断醒来,就返回true
return Thread.interrupted();
}
注意:Thread的interrupted方法在返回标记的时候会清除标记,也就是说如果中断醒来获得锁成功,则会返回true;如果中断醒来没有成功获得锁,则继续挂起,此时中断标记也会被清除,下一次醒来就是正常醒来的,acquireQueued方法只会返回false,表示是正常醒来的。
cancelAcquire()
最后我们看到finally模块,这里是对锁资源获取失败后进行一些善后工作,从acquireQueued()
方法中我们可以看到只有当tryAcquire方法抛出异常时才会进入此方法,也就是说AQS框架针对开发人员自己实现的获取锁操作如果抛出异常,也做了妥善的处理。下面看源码:
private void cancelAcquire(Node node) {
// 如果node不存在就忽略
if (node == null)
return;
node.thread = null;
// 跳过已经被取消的前驱节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//得到前置节点的后继节点,由于前面的跳节点操作,此时的前置节点的后继节点不一定是node了,此处可以好好想一想
Node predNext = pred.next;
//将当前节点的状态设置为CANCELLED,这样别的节点在处理时就会忽略它
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,则直接删除
// 此处不用CAS失败的问题,因为无论CAS是否成功,当前节点都已经被成功删除
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;
//这里的逻辑比较绕,大体意思是:如果当前节点的前置节点不是头结点 且 它后面的节点等待着它唤醒(waitStatus<0)
//且 它后面的节点没有被取消,则将它的前置和后继节点相连,相当于删除了当前节点
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//进入到这里,要么当前节点的前置节点是头结点,要么前置节点的waitStatus是PROPAGATE,直接唤醒当前节点的后置节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
上面就是独占模式获取锁的核心源码,容我缓一缓…
release()方法
下面看释放锁的过程,还是直接上源码:
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
Node h = head;
//如果头结点不为空且waitStatus不等于0,则释放头结点,返回true
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
//释放锁失败返回false
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//把标记设为0,表示唤醒操作已经开始,提高并发环境下性能
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//如果后继节点为空或者已经被取消
//则从尾部向前查找离node最近的需要唤醒的节点,注意!此处循环体内没有break,所以会一直向前找!
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//执行唤醒操作
LockSupport.unpark(s.thread);
}
这里给出一张从其他博客上看到的acquire方法的执行流程图:
总结
以上就是AQS独占锁的获取与释放过程,大致思想很简单,就是尝试去获取锁,如果失败就加入一个队列中挂起。释放锁时,如果队列中有等待的线程就进行唤醒。但是看源码,会发现细节好多好多。。以上是我结合自己的理解加上他人博客进行的一次总结,希望能对大家有所帮助,有错误之处还请各位指正。
觉得有用可以点个赞。^ _ ^