一 简介
上一篇总结了AQS的整体架构,以及它的组成,和它们之间的关系。AQS主要的三部分分别是volatile修饰的变量、同步队列和等待队列其中,同步队列在上篇总结中已经介绍过了,不知道的话可以点这里AQS的框架组成以及同步队列源码解析。这一片文章主要总结独占模式下资源的获取。
二 资源的获取源码解析
在上一篇总结中,最后过源码的时候看到addWater(),同时我们也提出两个猜想获取资源的两种方式
猜想一:线程上来就直接获取,如果获取成功的话那就执行了,获取失败的话被封装成节点添加到同步队列中
猜想二:线程一上来看看队列中有没有要同步的节点,如果有的话那就不获取资源了直接添加到同步队列中,等待上一个节点唤醒。
现在看一下操作stste的方法有哪些
/**
* 返回当前同步状态
*/
protected final int getState() {
return state;
}
/**
* 设置当前的同步状态
*/
protected final void setState(int newState) {
state = newState;
}
/**
* 使用CAS来更新state的值
*
* @param except 期望值
* @param update 更新值
* @return 更新是否成功
*/
protected final boolean compareAndSetState(int except, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, except, update);
}
AQS下面这个方法就是来获取资源state的
/**
* 以独占模式获取,忽略中断。实现 至少调用一次{@link #tryAcquire},
* 成功回归。 否则,线程可能会排队
*
* @param arg 资源请求
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
看到上面的代码我们看到了比较熟悉的方法就是addWaier(),这个方法返回一个封装好线程的节点,被当作参数传递到acquireQueued()这个方法中。但是acquireQueued执行不执行取决与前面的tryAcquire()这个方法。当tryAcquire()返回false的时候才会去执行acquireQueued()这个方法。再看一下tryAcquire()这个方法。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
尽然抛出一个异常,在第一遍的文章说过AQS是基于模板方法的框架,既然使用的是模板方法那就需要子类去实现了,在ReentrantLock内部类Sync中是AQS的实现。下面是源码
@Override
protected final boolean tryAcquire(int acquire) {
return nonfairTryAcquire(acquire);
}
final boolean nonfairTryAcquire(int acquires) {
//获取但当前线程
final Thread current = Thread.currentThread();
//获取资源的状态
int c = getState();
if (c == 0) {
//如果资源的状态为0的话说明state是没有线程持有当前资源的
if (compareAndSetState(0, acquires)) {
//使用CAS替换,如果成功那就将独占线程设为当前线程,也就意味着当前线程
//拥有执行时间了
setExclusiveOwnerThread(current);
//获取的资源返回true
return true;
}
} else if (current == getExclusiveOwnerThread()) {
//如果state不为0的话独占线程是当前线程的话那么给state加一,这里是
//重入锁的实现
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
//如果没获取到资源的话返回false
return false;
}
好了tryAcquire()方法干什么的已经知道了,如果获取到资源的话返回true,没有获取到资源的话就返回false,现在再看acquire()这个方法,当线程获取到资源的时候返回的是true ,!true也就是false,那就不必在在执行&&面的语句了,如果没有获取到资源,就要执行后面的语句了,首先将当前线程包装成一个节点添加到对列尾部并返回这个节点。既然包装了,那就要处理将这个节点了。acquireQueued方法就是干这个的,下面是源码
/**
* 将竞争节点设置为头节点,同时当前节点不是头节点的话
*
* @param node 要获取头节点的节点
* @param arg state状态参数
* @return 返回true表示线程发生了中断
*/
final boolean acquireQueued(final Node node, int arg) {
//这个变量来看是否要取消节点的竞争
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
//获取node的前驱节点
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {//当前的前驱节点是头节点,那么当前节点就获取资源
//将node设置为头节点
setHead(node);
//消除引用有利于垃圾回收
p.next = null;
failed = false;
return interrupted;
}
//这里是for循环的移动条件跳过前驱接节点未取消状态的节点,
//当前线程中断的话只能返回去等待了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
//当线程中断直接取消当前节点竞争
if (failed) {
cancelAcquire(node);
}
}
}
对于上面一段源码来说,获取资源是不难理解的,但是没有获取到资源时候执行了一个if语句,看一下if语句中两个方法中分别做了什么。下面是源码
/**
* 检查是否可以在节点后添加竞争节点,同时检查node前驱节点是否取消,如果取消了就要将这个节点
* 移除掉,如果在前驱节点等待中返回true
*
* @param pred 前驱节点
* @param node 当前节点
* @return 返回boolean
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) {
return true;
}
if (ws > 0) {
do {
//如果前驱节点的的状态为取消状态那么跳过直到找到可以
//获取竞争的node
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
当node中的前驱节点是等待中的时候就会执行下一个方法,下一个方法的源码如下
/**
* 将线程至于waiting中 并返回线程是否中断
*
* @return 中断线程的boolean
*/
private final boolean parkAndCheckInterrupt() {
//将线程至于waiting中
LockSupport.park(this);
return Thread.interrupted();
}
看到这里基本上已经清楚了,acquireQueued()方法就是如果可以获取到资源的时候直接获取,不能获取到资源时检查父节点是否在等待状态中,如果在等待中,就调用LockSupport.park(),将当前线程至于等待状态,等待中断或着唤醒。AQS获取资源也就完了。
AQS获取资源的总结:
1.首先先使用CAS获取支援state。如果获取成功的话就不在添加节点了,如果获取失败的话将当前线程封装到node中。
2.在添加节点的时候判断前驱节点是否是头节点,如果前驱节点是头节点的话,继续for循环获取资源。如果不是头节点的话检查当前的前驱节点是否为空,将当前节点的所有前面的节点为取消状态的全都去掉。去掉之后当前节点还不是头节点时,将线程至于waiting状态等待中断或唤醒。
上面的总结也就验证了的猜想一,其实就是非公平锁的实现,猜想二是公平的锁的实现。可以在ReentrantLock中的内部类FairSync看到公平锁的实现。
三 资源的释放
关于支援的释放我认为是比较简单的,可以大体的猜想一下,找到持有资源的node节点,将state的值设置为0,再将当前node节点从同步队列中移除掉。然后唤醒下一个节点的线程。下面是源码
/**
* 释放资源,并唤醒下一个节点
*
* @param arg 状态
* @return 释放成功
*/
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()方法中下面是源码。
/**
* 线程调用释放锁
*
* @param arg 状态
* @return 是否释放成功
*/
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
这个方法和tryAcquire()方法一样都是抛了一个异常我们看其中子类的实现。下面是源码。
/**
* 释放资源
*
* @param release 释放锁的数字
* @return 是否释放成功
*/
@Override
protected final boolean tryRelease(int release) {
//获取到资源并减去资源
int c = getState() - release;
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean free = false;
if (c == 0) {
free = true;
//当state为0的时候表示资源释放完成想独占的线程设置为null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
四 总结
从上面的源码中我们可以看出,锁的实现无非就是资源的获取,与队列的操作,线程状态的转化。
获取资源时:先操作state,操作成功就直接获取到了,操作不成功添加到同步队列中,调用LockSuppport.park(),将线程至于waiting状态等待中断或唤醒。
资源释放:先操作state,操作成功的话将队列中的当前节点移除,唤醒下一个节点。