详细介绍了AQS中的同步队列以及同步状态的独占式获取、释放的原理。
AQS相关文章:
AQS(AbstractQueuedSynchronizer)源码深度解析(1)—AQS的设计与总体结构
AQS(AbstractQueuedSynchronizer)源码深度解析(2)—Lock接口以及自定义锁的实现
AQS(AbstractQueuedSynchronizer)源码深度解析(3)—同步队列以及独占式获取锁、释放锁的原理【一万字】
AQS(AbstractQueuedSynchronizer)源码深度解析(4)—共享式获取锁、释放锁的原理【一万字】
AQS(AbstractQueuedSynchronizer)源码深度解析(5)—条件队列的等待、通知的实现以及AQS的总结【一万字】
AQS中的同步队列与同步状态的获取、释放、阻塞有紧密的关联关系,这两个知识点必须要连起来学习。
public abstract class AbstractQueuedSynchronizer extends
AbstractOwnableSynchronizer implements java.io.Serializable {
/**
* 当前获取锁的线程,该变量定义在父类中,AQS直接继承。在独占锁的获取时,如果是重入锁,那么需要知道到底是哪个线程获得了锁。没有就是null
*/
private transient Thread exclusiveOwnerThread;
/**
* AQS中保持的对同步队列的引用
* 队列头结点,实际上是一个哨兵结点,不代表任何线程,head所指向的Node的thread属性永远是null。
*/
private transient volatile Node head;
/**
* 队列尾结点,后续的结点都加入到队列尾部
*/
private transient volatile Node tail;
/**
* 同步状态
*/
private volatile int state;
/**
* Node内部类,同步队列的结点类型
*/
static final class Node {
/*AQS支持共享模式和独占模式两种类型,下面表示构造的结点类型标记*/
/**
* 共享模式下构造的结点,用来标记该线程是获取共享资源时被阻塞挂起后放入AQS 队列的
*/
static final Node SHARED = new Node();
/**
* 独占模式下构造的结点,用来标记该线程是获取独占资源时被阻塞挂起后放入AQS 队列的
*/
static final Node EXCLUSIVE = null;
/*线程结点的等待状态,用来表示该线程所处的等待锁的状态*/
/**
* 指示当前结点(线程)需要取消等待
* 由于在同步队列中等待的线程发生等待超时、中断、异常,即放弃获取锁,需要从同步队列中取消等待,就会变成这个状态
* 如果结点进入该状态,那么不会再变成其他状态
*/
static final int CANCELLED = 1;
/**
* 指示当前结点(线程)的后续结点(线程)需要取消等待(被唤醒)
* 如果一个结点状态被设置为SIGNAL,那么后继结点的线程处于挂起或者即将挂起的状态
* 当前结点的线程如果释放了锁或者放弃获取锁并且结点状态为SIGNAL,那么将会尝试唤醒后继结点的线程以运行
* 这个状态通常是由后继结点给前驱结点设置的。一个结点的线程将被挂起时,会尝试设置前驱结点的状态为SIGNAL
*/
static final int SIGNAL = -1;
/**
* 线程在等待队列里面等待,waitStatus值表示线程正在等待条件
* 原本结点在等待队列中,结点线程等待在Condition上,当其他线程对Condition调用了signal()方法之后
* 该结点会从从等待队列中转移到同步队列中,进行同步状态的获取
*/
static final int CONDITION = -2;
/**
* 释放共享资源时需要通知其他结点,waitStatus值表示下一个共享式同步状态的获取应该无条件传播下去
*/
static final int PROPAGATE = -3;
/**
* 记录当前线程等待状态值,包括以上4中的状态,还有0,表示初始化状态
*/
volatile int waitStatus;
/**
* 前驱结点,当结点加入同步队列将会被设置前驱结点信息
*/
volatile Node prev;
/**
* 后继结点
*/
volatile Node next;
/**
* 当前获取到同步状态的线程
*/
volatile Thread thread;
/**
* 等待队列中的后继结点,如果当前结点是共享模式的,那么这个字段是一个SHARED常量
* 在独占锁模式下永远为null,仅仅起到一个标记作用,没有实际意义。
*/
Node nextWaiter;
/**
* 如果是共享模式下等待,那么返回true(因为上面的Node nextWaiter字段在共享模式下是一个SHARED常量)
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 用于建立初始头结点或SHARED标记
*/
Node() {
}
/**
* 用于添加到等待队列
*
* @param thread
* @param mode
*/
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//......
}
}
}
由上面的源码可知,同步队列的基本结构如下图:
在AQS内部Node的源码中我们能看到,同步队列是"CLH" (Craig, Landin, andHagersten) 锁队列的变体,它的head引用指向的头结点作为哨兵结点,不存储任何与等待线程相关的信息,或者可以看成已经获得锁的结点。第二个结点开始才是真正的等待线程构建的结点,后续的结点会加入到链表尾部。
将新结点添加到链表尾部的方法是compareAndSetTail(Node expect,Node update)方法,该方法是一个CAS方法,能够保证线程安全。
最终获取锁的线程所在的结点,会被设置成为头结点(setHead方法),该设置步骤是通过获取锁成功的线程来完成的,由于只有一个线程能够成功获取到锁,因此设置的方法并不需要使用CAS来保证。
同步队列遵循先进先出(FIFO),头结点的next结点是将要获取到锁的结点,线程在释放锁的时候将会唤醒后继结点,然后后继结点会尝试获取锁。
Lock中“锁”的状态使用state变量来表示,一般来说0表示锁没被占用,大于0表示所已经被占用了。
AQS提供的锁的获取和释放分为独占式的和共享式的:
对于AQS 来说,线程同步的关键是对同步状态state的操作:
void acquire( int arg) 、void acquirelnterruptibly(int arg) 、boolean release( int arg)
。void acquireShared(int arg) 、void acquireSharedInterruptibly(int arg)、 boolean reaseShared(int arg)
。获取锁的大概通用流程如下:
线程会首先尝试获取锁,如果失败,则将当前线程以及等待状态等信息包成一个Node结点加到同步队列里。接着会不断循环尝试获取锁(获取锁的条件是当前结点为head的直接后继才会尝试),如果失败则会尝试阻塞自己(阻塞的条件是当前节结点的前驱结点是SIGNAL状态),阻塞后将不会执行后续代码,直至被唤醒;当持有锁的线程释放锁时,会唤醒队列中的后继线程,或者阻塞的线程被中断或者时间到了,那么阻塞的线程也会被唤醒。
如果分独占式和共享式,那么在上面的通用步骤之下有这些区别:
实际上,具体的步骤更加复杂,下面讲解源码的时候会提到!
通过调用AQS的acquire模版方法可以独占式的获取锁,该方法不会响应中断,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。基于独占式实现的组件有ReentrantLock等。
该方法大概步骤如下:
/**
* 独占式的尝试获取锁,一直获取不成功就进入同步队列等待
*/
public final void acquire(int arg) {
//内部是由4个方法的调用组成的
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
熟悉的tryAcquire方法,这个方法我们在最开头讲“AQS的设计”时就提到过,该方法是AQS的子类即我们自己实现的,用于首次尝试获取独占锁,一般来说就是对state的改变、或者重入锁的检查、设置当前获得锁的线程等等,不同的锁有自己相应的逻辑判断,这里不多讲,后面讲具体锁的实现的时候(比如ReentrantLock)会讲到。总之,获取成功该方法就返回true,失败就返回false。
在AQS的中tryAcquire的实现为抛出异常,因此需要子类重写:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
addWaiter方法是AQS提供的,也不需要我们重写,或者说是锁的通用方法!
addWaiter方法用于将按照独占模式构造的同步结点Node.EXCLUSIVE添加到同步队列的尾部。大概步骤为:
/**
* addWaiter(Node node)方法将获取锁失败的线程构造成结点加入到同步队列的尾部
*
* @param mode 模式。独占模式传入的是一个Node.EXCLUSIVE,即null;共享模式传入的是一个Node.SHARED,即一个静态结点对象(共享的、同一个)
* @return 返回构造的结点
*/
private Node addWaiter(Node mode) {
/*1 首先构造结点*/
Node node = new Node(Thread.currentThread(), mode);
/*2 尝试将结点直接放在队尾*/
//直接获取同步器的tail结点,使用pred来保存
Node pred = tail;
/*如果pred不为null,实际上就是队列不为null
* 那么使用CAS方式将当前结点设为尾结点
* */
if (pred != null) {
node.prev = pred;
//通过使用compareAndSetTail的CAS方法来确保结点能够被线程安全的添加,虽然不一定能成功。
if (compareAndSetTail(pred, node)) {
//将新构造的结点置为原队尾结点的后继
pred.next = node;
//返回新结点
return node;
}
}
/*
* 3 走到这里,可能是:
* (1) 由于可能是并发条件,并且上面的CAS操作并没有循环尝试,因此可能添加失败
* (2) 队列可能为null
* 调用enq方法,采用自旋方式保证构造的新结点成功添加到同步队列中
* */
enq(node);
return node;
}
/**
* addWaiter方法中使用到的Node构造器
*
* @param thread 当前线程
* @param mode 模式
*/
Node(Thread thread, AbstractQueuedSynchronizer.Node mode) {
//等待队列中的后继结点 就等于该结点的模式
//由此可知,共享模式该值为Node.SHARED结点常量,独占模式该值为null
this.nextWaiter = mode;
//当前线程
this.thread = thread;
}
enq方法用在同步队列为null或者一次CAS添加失败的时候,enq要保证结点最终必定添加成功。大概步骤为:
enq方法返回的是新结点的前驱,当然在addWaiter方法中没有用到。
另外,添加头结点使用的compareAndSetHead方法和添加尾结点使用的compareAndSetTail方法都是CAS方法,并且都是调用Unsafe类中的本地方法,因为线程挂机、恢复、CAS操作等最终会通过操作系统中实现,Unsafe类就提供了Java与底层操作系统进行交互的直接接口,这个类的里面的许多操作类似于C的指针操作,通过找到对某个属性的偏移量,直接对该属性赋值,因为与Java本地方法对接都是Hospot源码中的方法,而这些的方法都是采用C++写的,必须使用指针!
也可以说Unsafe是AQS的实现并发控制机制基石。因此在学习AQS的时候,可以先了解Unsafe:Java中的Unsafe类的原理详解与使用案例。
/**
* 循环,直到尾结点添加成功
*/
private Node enq(final Node node) {
/*死循环操作,直到添加成功*/
for (; ; ) {
//获取尾结点t
Node t = tail;
/*如果队列为null,则初始化同步队列*/
if (t == null) {
/*调用compareAndSetHead方法,初始化同步队列
* 注意:这里是新建了一个空白结点,这就是传说中的哨兵结点
* CAS成功之后,head将指向该哨兵结点,返回true
* */
if (compareAndSetHead(new Node()))
//尾结点指向头结点(哨兵结点)
tail = head;
/*之后并没有结束,而是继续循环,此时队列已经不为空了,因此会进行下面的逻辑*/
}
/*如果队列不为null,则和外面的的方法类似,调用compareAndSetTail方法,新建新结点到同步队列尾部*/
else {
/*1 首先修改新结点前驱的指向,这一步不是安全的
但是没关系,因为这一步如果发生了冲突,那么下面的CAS操作必然之后有一条线程会成功
其他线程将会重新循环尝试*/
node.prev = t;
/*
* 2 调用compareAndSetTail方法通过CAS方式尝试将结点添加到同步队列尾部
* 如果添加成功,那么才能继续下一步,结束这个死循环,否则就会不断循环尝试添加
* */
if (compareAndSetTail(t, node)) {
//3 修改原尾结点后继结点的指向
t.next = node;
//返回新结点,结束死循环
return t;
}
}
}
}
/**
* CAS添加头结点. 仅仅在enq方法中用到
*
* @param update 头结点
* @return true 成功;false 失败
*/
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
/**
* CAS添加尾结点. 仅仅在enq方法中用到
*
* @param expect 预期原尾结点
* @param update 新尾结点
* @return true 成功;false 失败
*/
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
在addWaiter和enq方法中,成为尾结点需要三步:
由于第二步设置tail是CAS操作,那么只能保证node的前驱prev一定是正确的,但是此后设置后继的操作却不一定能够马上成功就切换到了其他线程,此时next可能为null,但实际他的后继并不一定真的为null。
因此同步队列只能保证前驱prev一定是可靠的,但是next却不一定可靠,所以后面的源码的遍历操作基本上都是从后向前通过前驱prev进行遍历的。
能够走到该方法,那么说明通过了tryAcquire()和addWaiter()方法,表示该线程获取锁已经失败并且被放入同步队列尾部了。
acquireQueued方法表示结点进入同步队列之后的动作,实际上就进入了一个自旋的过程,自旋过程中,当条件满足,获取到了锁,就可以从这个自旋中退出并返回,否则可能会阻塞该结点的线程,后续即使阻塞被唤醒,还是会自旋尝试获取锁,直到成功或者而抛出异常。
最终如果该方法会因为获取到锁而退出,则会返回否被中断标志的标志位 或者 因为异常而退出,则会抛出异常!大概步骤为:
/**
* @param node 新结点
* @param arg 参数
* @return 如果在等待时中断,则返回true
*/
final boolean acquireQueued(final Node node, int arg) {
//failed表示获取锁是否失败标志
boolean failed = true;
try {
//interrupted表示是否被中断标志
boolean interrupted = false;
/*死循环*/
for (; ; ) {
//获取新结点的前驱结点
final Node p = node.predecessor();
/*只有前驱结点是头结点的时候才能尝试获取锁
* 同样调用tryAcquire方法获取锁
* */
if (p == head && tryAcquire(arg)) {
//获取到锁之后,就将自己设置为头结点(哨兵结点),线程出队列
setHead(node);
//前驱结点(原哨兵结点)的链接置空,由JVM回收
p.next = null;
//获取锁是否失败改成false,表示成功获取到了锁
failed = false;
//返回interrupted,即返回线程是否被中断
return interrupted;
}
/*前驱结点不是头结点或者获取同步状态失败*/
/*shouldParkAfterFailedAcquire检测线程是否应该被挂起,如果返回true
* 则调用parkAndCheckInterrupt用于将线程挂起
* 否则重新开始循环
* */
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
/*到这一步,说明是当前结点(线程)因为被中断而唤醒,那就改变自己的中断标志位状态信息为true
* 然后又从新开始循环,直到获取到锁,才能返回
* */
interrupted = true;
}
}
/*线程获取到锁或者发生异常之后都会执行的finally语句块*/ finally {
/*如果failed为true,表示获取锁失败,即对应发生异常的情况,
这里发生异常的情况只有在tryAcquire方法和predecessor方法中可能会抛出异常,此时还没有获得锁,failed=true
那么执行cancelAcquire方法,该方法用于取消该线程获取锁的请求,将该结点的线程状态改为CANCELLED,并尝试移除结点(如果是尾结点)
另外,在超时等待获取锁的的方法中,如果超过时间没有获取到锁,也会调用该方法
如果failed为false,表示获取到了锁,那么该方法直接结束,继续往下执行;*/
if (failed)
//取消获取锁请求,将当前结点从队列中移除,
cancelAcquire(node);
}
}
/**
* 位于Node结点类中的方法
* 返回上一个结点,或在 null 时引发 NullPointerException。 当前置不能为空时使用。 空检查可以取消,表示此异常无代码层面的意义,但可以帮助 VM?所以这个异常到底有啥用?
*
* @return 此结点的前驱
*/
final Node predecessor() throws NullPointerException {
//获取前驱
Node p = prev;
//如果为null,则抛出异常
if (p == null)
throw new NullPointerException();
else
//返回前驱
return p;
}
/**
* head指向node新结点,该方法是在tryAcquire获取锁之后调用,不会产生线程安全问题
*
* @param node 新结点
*/
private void setHead(Node node) {
head = node;
//新结点的thread和prev属性置空
//即丢弃原来的头结点,新结点成为哨兵结点,内部线程出队
//设置里虽然线程引用置空了,但是一般在tryAcquire方法中轨记录获取到锁的线程,因此不担心找不到是哪个线程获取到了锁
//这里也能看出,哨兵结点或许也可以叫做"获取到锁的结点"
node.thread = null;
node.prev = null;
}
shouldParkAfterFailedAcquire方法在没有获取到锁之后调用,用于判断当前结点是否需要被挂起。大概步骤如下:
只有前驱结点状态为SIGNAL时,当前结点才能安心挂起,否则一直自旋!
从这里能看出来,一个结点的SIGNAL状态一般都是由它的后继结点设置的,但是这个状态却是表示后继结点的状态,表示的意思就是前驱结点如果释放了锁,那么就有义务唤醒后继结点!
/**
* 检测当前结点(线程)是否应该被挂起
*
* @param pred 该结点的前驱
* @param node 该结点
* @return 如果前驱结点已经是SIGNAL状态,当前结点才能挂起,返回true;否则,可能会查找新的前驱结点或者尝试将前驱结点设置为SIGNAL状态,返回false
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取 前取的waitStatus_等待状态
//回顾创建结点时候,并没有给waitStatus赋值,因此每一个结点最开始的时候waitStatus的值都为0
int ws = pred.waitStatus;
/*如果前驱结点已经是SIGNAL状态,即表示当前结点可以挂起*/
if (ws == Node.SIGNAL)
return true;
/*如果前驱结点状态大于0,即 Node.CANCELLED 表示前驱结点放弃了锁的等待*/
if (ws > 0) {
/*由该前驱向前查找,直到找到一个状态小于等于0的结点(即没有被取消的结点),当前结点成为该结点的后驱,这一步很重要,可能会清理一段被取消了的结点,并且如果该前驱释放了锁,还会唤醒它的后继,保持队列活性*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}
/*否则,前驱结点的状态既不是SIGNAL(-1),也不是CANCELLED(1)*/
else {
/*前驱结点的状态CAS设置为SIGNAL(-1),可能失败,但没关系,因为失败之后会一直循环*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回false,表示当前结点不能挂起
return false;
}
shouldParkAfterFailedAcquire方法返回true之后,将会调用parkAndCheckInterrupt方法挂起线程并且后续判断中断状态,分两步:
/**
* 挂起线程,在线程返回后返回中断状态
*
* @return 如果因为线程中断而返回,而返回true,否则返回false
*/
private final boolean parkAndCheckInterrupt() {
/*1)使用LockSupport.park(this)挂起该线程,不再执行后续的步骤、代码。直到该线程被中断或者被唤醒(unpark)*/
LockSupport.park(this);
/*2)如果该线程被中断或者唤醒,那么返回Thread.interrupted()方法的返回值,
该方法用于判断前线程的中断状态,并且清除该中断状态,即,如果该线程因为被中断而唤醒,则中断状态为true,将中断状态重置为false,并返回true,注意park方法被中断时不会抛出异常!
如果该线程不是因为中断被唤醒,则中断状态为false,并返回false*/
return Thread.interrupted();
}
在acquireQueued方法中,具有一个finally代码块,那么无论try中发生了什么,finally代码块都会执行的。在acquire独占式不可中断获取锁的方法中,执行finally的只有两种情况:
finally代码块中的逻辑为:
综上所述,在acquire独占式不可中断获取锁的方法中,大部分情况在finally中都是什么也不干就返回了,或者说抛出异常的情况基本没有,因此cancelAcquire方法基本不考虑。
但是在可中断获取锁或者超时获取锁的方法中,执行到cancelAcquire方法的情况还是比较常见的。因此将cancelAcquire方法的源码分析放到可中断获取锁方法的源码分析部分!
selfInterrupt是acquire中最后可能调用的一个方法,顾名思义,用于自我中断,什么意思呢,就是根据!tryAcquire和acquireQueued返回值判断是否需要设置中断标志位。
只有tryAcquire尝试失败,并且acquireQueued方法true时,才表示该线程是被中断过了的,但是在parkAndCheckInterrupt里面判断中断标志位之后又重置的中断标志位(interrupted方法会重置中断标志位)。
虽然看起来没啥用,但是本着负责的态度,还是将中断标志位记录下来。那么此时重新设置该线程的中断标志位为true。
/**
* 中断当前线程,由于此时当前线程出于运行态,因此只会设置中断标志位,并不会抛出异常
*/
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
当前线程获取到锁并执行了相应逻辑之后,就需要释放锁,使得后续结点能够继续获取锁。通过调用AQS的release(int arg)模版方法可以独占式的释放锁,在该方法大概步骤如下:
/**
* 独占式的释放同步状态
*
* @param arg 参数
* @return 释放成功返回true, 否则返回false
*/
public final boolean release(int arg) {
/*tryRelease释放同步状态,该方法是自己重写实现的方法
释放成功将返回true,否则返回false或者自己实现的逻辑*/
if (tryRelease(arg)) {
//获取头结点
Node h = head;
//如果头结点不为null并且状态不等于0
if (h != null && h.waitStatus != 0)
/*那么唤醒头结点的一个出于等待锁状态的后继结点
* 该方法在acquire中已经讲过了
* */
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor用于唤醒参数结点的某个非取消的后继结点,该方法在很多地方法都被调用,大概步骤:
/**
* 唤醒指定结点的后继结点
*
* @param node 指定结点
*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
/*
* 1) 如果当前结点的状态小于0,那么CAS设置为0,表示后继结点线程可以先尝试获锁,而不是直接挂起。
* */
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//先获取node的直接后继
Node s = node.next;
/*
* 2) 如果s为null或者状态为取消CANCELLED,则从tail开始到node之间倒序向前查找,找到离tail最远的非取消结点赋给s。
* */
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
/*
* 3)如果s不为null,那么状态肯定不是取消CANCELLED,则直接唤醒s的线程,调用LockSupport.unpark方法唤醒,被唤醒的结点将从被park的位置向后执行!
* */
if (s != null)
LockSupport.unpark(s.thread);
}
在JDK1.5之前,当一个线程获取不到锁而被阻塞在synchronized之外时,如果对该线程进行中断操作,此时该线程的中断标志位会被修改,但线程依旧会阻塞在synchronized上,等待着获取锁,即无法响应中断。
上面分析的独占式获取锁的方法acquire,同样是不会响应中断的。但是AQS提供了另外一个acquireInterruptibly模版方法,调用该方法的线程在等待获取锁时,如果当前线程被中断,会立刻返回,并抛出InterruptedException。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果当前线程被中断,直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取锁
if (!tryAcquire(arg))
//如果没获取到,那么调用AQS 可被中断的方法
doAcquireInterruptibly(arg);
}
doAcquireInterruptibly会首先判断线程是否是中断状态,如果是则直接返回并抛出异常其他不步骤和独占式不可中断获取锁基本原理一致。还有一点的区别就是在后续挂起的线程因为线程被中断而返回时的处理方式不一样:
/**
* 独占可中断式的锁获取
*
* @param arg 参数
*/
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//同样调用addWaiter将当前线程构造成结点加入到同步队列尾部
final Node node = addWaiter(Node.EXCLUSIVE);
//获取锁失败标志,默认为true
boolean failed = true;
try {
/*和独占式不可中断方法acquireQueued一样,循环获取锁*/
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
/*
* 这里就是区别所在,独占不可中断式方法acquireQueued中
* 如果线程被中断,此处仅仅会记录该状态,interrupted = true,紧接着又继续循环获取锁
*
* 但是在该独占可中断式的锁获取方法中
* 如果线程被中断,此处直接抛出异常,因此会直接跳出循环去执行finally代码块
* */
throw new InterruptedException();
}
}
/*获取到锁或者抛出异常都会执行finally代码块*/
finally {
/*如果获取锁失败。可能就是线程被中断了,那么执行cancelAcquire方法取消该结点对锁的请求,该线程结束*/
if (failed)
cancelAcquire(node);
}
}
在doAcquireInterruptibly方法中,具有一个finally代码块,那么无论try中发生了什么,finally代码块都会执行的。在acquireInterruptibly独占式可中断获取锁的方法中,执行finally的只有两种情况:
finally代码块中的逻辑为:
由于独占式可中断获取锁的方法中,线程被中断而抛出异常的情况比较常见,因此这里分析finally中cancelAcquire的源码。cancelAcquire方法用于取消结点获取锁的请求,参数为需要取消的结点node,大概步骤为:
/**
* 取消指定结点获取锁的请求
*
* @param node 指定结点
*/
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
/*1 node记录的线程thread置为null*/
node.thread = null;
/*2 类似于shouldParkAfterFailedAcquire方法中查找有效前驱的代码:
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
这里同样由node向前查找,直到找到一个状态小于等于0的结点(即没有被取消的结点),作为前驱
但是这里只更新了node.prev,没有更新pred.next*/
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//predNext记录pred的后继,后续CAS会用到。
Node predNext = pred.next;
/*3 node的等待状态设置为CANCELLED,即取消请求锁*/
node.waitStatus = Node.CANCELLED;
/*4 如果当前结点是尾结点,那么尝试CAS更新tail指向pred,成功之后继续CAS设置pred.next为null。*/
if (node == tail && compareAndSetTail(node, pred)) {
//新尾结点pred的next结点设置为null,即使失败了也没关系,说明有其它新入队线程或者其它取消线程更新掉了。
compareAndSetNext(pred, predNext, null);
}
/*5 否则,说明node不是尾结点或者CAS失败(可能存在对尾结点的并发操作),这种情况要做的事情是把pred和node的后继非取消结点拼起来。*/
else {
int ws;
/*5.1 如果node不是head的后继 并且 (pred的状态为SIGNAL或者将pred的waitStatus置为SIGNAL成功) 并且 pred记录的线程不为null。
那么设置pred.next指向node.next。这里没有设置prev,但是没关系。
此时pred的后继变成了node的后继—next,后续next结点如果获取到锁,那么在shouldParkAfterFailedAcquire方法中查找有效前驱时,
也会找到这个没取消的pred,同时将next.prev指向pred,也就设置了prev关系了。
*/
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//获取next结点
Node next = node.next;
//如果next结点存在且未被取消
if (next != null && next.waitStatus <= 0)
//那么CAS设置perd.next指向node.next
compareAndSetNext(pred, predNext, next);
}
/*5.2 否则,说明node是head的后继 或者pred状态设置失败 或者 pred记录的线程为null。
*
* 此时需要调用unparkSuccessor方法尝试唤醒node结点的后继结点,因为node作为head的后继结点是唯一有资格取尝试获取锁的结点。
* 如果外部线程A释放锁,但是还没有调用unpark唤醒node的时候,此时node被中断或者发生异常,这时node将会调用cancelAcquire取消,结点内部的记录线程变成null,
* 此时就是算A线程的unpark方法执行,也只是LockSupport.unpark(null)而已,也就不会唤醒任何结点了
* 那么node后面的结点也不会被唤醒了,队列就失活了;如果在这种情况下,在node将会调用cancelAcquire取消的代码中
* 调用一次unparkSuccessor,那么将唤醒被取消结点的后继结点,让后继结点可以尝试获取锁,从而保证队列活性!
*
* 前面对node进行取消的代码中,并没有将node彻底移除队列,
* 而被唤醒的结点会尝试获取锁,而在在获取到锁之后,在
* setHead(node);
* p.next = null; // help GC
* 部分,可能将这些被取消的结点清除
* */
else {
unparkSuccessor(node);
}
/*最后node.next指向node自身,方便后续GC时直接销毁无效结点
同时也是为了Condition的isOnSyncQueue方法,判断一个原先属于条件队列的结点是否转移到了同步队列。
因为同步队列中会用到结点的next域,取消结点的next也有值的话,可以断言next域有值的结点一定在同步队列上。
这里也能看出来,遍历的时候应该采用倒序遍历,否则采用正序遍历可能出现死循环*/
node.next = node;
}
}
设一个同步队列结构如下,有ABCDE五个线程调用acquireInterruptibly方法争夺锁,并且BCDE线程都是因为获取不到锁而导致的阻塞。
我们来看看几种情况下cancelAcquire方法怎么处理的:
如果此时线程D被中断,那么抛出异常进入finally代码块,属于node不是尾结点,node不是head的后继的情况,如下图:
在cancelAcquire方法之后的结构如下:
如果此时线程E被中断,那么抛出异常进入finally代码块,属于node是尾结点的情况,如下图:
在cancelAcquire方法之后的结构如下:
如果此时进来了两个新线程F、G,并且又都被挂起了,那么此时同步队列结构如下图:
可以看到,实际上该队列出现了分叉,这种情况在同步队列中是很常见的,因为被取消的结点并没有主动去除自己的prev引用。那么这部分被取消的结点无法被删除吗,其实是可以的,只不过需要满足一定的条件结构!
如果此时线程B被中断,那么抛出异常进入finally代码块,属于node不是尾结点,node是head的后继的情况,如下图:
在cancelAcquire方法之后的结构如下:
注意在这种情况下,node还会调用unparkSuccessor方法唤醒后继结点C,让C尝试获取锁,如果假设此时线程A的锁还没有使用完毕,那么此时C肯定不能获取到锁。
但是C也不是什么都没做,C在被唤醒之后获得CPU执行权的那段时间里,在doAcquireInterruptibly方法的for循环中,改变了一些引用关系。
它会判断自己是否可以被挂起,此时它的前驱被取消了waitStatus=1,明显不能,因此会继续向前寻找有效的前驱,具体的过程在前面的“acquire- acquireQueued”部分有详解,最终C被挂起之后的结构如下:
可以看到C最终和head结点直接链接了起来,但是此时被取消的B由于具有prev引用,因此还没有被GC,不要急,这是因为还没到指定结构,到了就自然会被GC了。
如果此时线程A的资源使用完毕,那么首先释放锁,然后会尝试唤醒一个没有取消的后继线程,明显选择C。
如果在A释放锁之后,调用LockSupport.unpark方法唤醒C之前,C被先一步因中断而唤醒了。此时C抛出异常,不会再去获得锁,而是去finally执行cancelAcquire方法去了,此时还是属于node不是尾结点,node是head的后继的情况,如下图:
那么在C执行完cancelAcquire方法之后的结构如下:
如果此时线程A又获取到了CPU的执行权,执行LockSupport.unpark,但此时结点C因为被中断而取消,其内部记录的线程变量变成了null,LockSupport.unpark(null),将会什么也不做。那么这时队列岂不是失活了?其实并没有!
此时,cancelAcquire方法中的“node不是尾结点,node是head的后继”这种情况下的unparkSuccessor方法就非常关键了。该方法用于唤醒被取消结点C的一个没被取消的后继结点F,让其尝试获取锁,这样就能保证队列不失活。
F被唤醒之后,会判断是否能够休眠,明显不能,因为前驱node的状态为1,此时经过循环中一系列方法的操作,会变成如下结构:
明显结点F是head的直接后继,可以获取锁。
在获取锁成功之后,F会将自己设置为新的head,此时又会改变一些引用关系,即将F与前驱结点的prev和next关系都移除:
setHead(node);
p.next = null; // help GC
引用关系改变之后的结构下:
可以看到,到这一步,才会真正的将哪些无效结点删除,被GC回收。那么,需要真正删除一个结点需要有什么条件?条件就是:如果某个结点获取到了锁,那么该结点的前驱以及和该结点前驱相关的结点都将会被删除!
但是,在上面的分析中,我们认为只要有节点引用关联就不会被GC回收,然而实际上现代Java虚拟机采用可达性分析算法来分析垃圾,因此,在上面的队列中,对于那些“分叉”,即那些被取消的、只剩下prev引用的、最重要的是不能通过head和tail的引用链到达也没有外部引用可达的节点,将会在可达性分析算法中被标记为垃圾并在一次GC中被直接回收!
比如此时F线程执行完了,下一个就是G,那么G获得锁之后,F将会被删除,最终结构如下:
独占式超时获取锁tryAcquireNanos模版方法可以被视作响应中断获取锁acquireInterruptibly方法的“增强版”,支持中断,支持超时时间!
/**
* 独占式超时获取锁,支持中断
*
* @param arg 参数
* @param nanosTimeout 超时时间,纳秒
* @return 是否获取锁成功
* @throws InterruptedException 如果被中断,则抛出InterruptedException异常
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//如果当前线程被中断,直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//同样调用tryAcquire尝试获取锁,如果获取成功则直接返回true
//否则调用doAcquireNanos方法挂起指定一段时间,该短时间内获取到了锁则返回true,超时还未获取到锁则返回false
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上, 增加了超时获取的特性。
该方法在自旋过程中,当结点的前驱结点为头结点时尝试获取锁,如果获取成功则从该方法返回,这个过程和独占式同步获取的过程类似,但是在锁获取失败的处理上有所不同。
如果当前线程获取锁失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。
因此,在超时非常短的场景下,AQS会进入无条件的快速自旋而不是挂起线程。
static final long spinForTimeoutThreshold = 1000L;
/**
* 独占式超时获取锁
*
* @param arg 参数
* @param nanosTimeout 剩余超时时间,纳秒
* @return true 成功 ;false 失败
* @throws InterruptedException 如果被中断,则抛出InterruptedException异常
*/
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//获取当前的纳秒时间
long lastTime = System.nanoTime();
//同样调用addWaiter将当前线程构造成结点加入到同步队列尾部
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
/*和独占式不可中断方法acquireQueued一样,循环获取锁*/
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
/*这里就是区别所在*/
//如果剩余超时时间小于0,则退出循环,返回false,表示没获取到锁
if (nanosTimeout <= 0)
return false;
//如果需要挂起 并且 剩余nanosTimeout大于spinForTimeoutThreshold,即大于1000纳秒
if (shouldParkAfterFailedAcquire(p, node)
&& nanosTimeout > spinForTimeoutThreshold)
//那么调用LockSupport.parkNanos方法将当前线程挂起nanosTimeout
LockSupport.parkNanos(this, nanosTimeout);
//获取当前纳秒,走到这一步可能是线程中途被唤醒了
long now = System.nanoTime();
//计算 新的剩余超时时间:原剩余超时时间 - (当前时间now - 上一次计算时的时间lastTime)
nanosTimeout -= now - lastTime;
//lastIme赋值为本次计算时的时间
lastTime = now;
//如果线程被中断了,那么直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
}
}
/*获取到锁、超时时间到了、抛出异常都会执行finally代码块*/
finally {
/*如果获取锁失败。可能就是线程被中断了,那么执行cancelAcquire方法取消该结点对锁的请求,该线程结束
* 或者是超时时间到了,那么执行cancelAcquire方法取消该结点对锁的请求,将返回false
* */
if (failed)
cancelAcquire(node);
}
}
在doAcquireNanos方法中,具有一个finally代码块,那么无论try中发生了什么,finally代码块都会执行的。在tryAcquireNanos独占式超时获取锁的方法中,执行finally的只有三种情况:
finally代码块中的逻辑为:
独占式的获取锁和释放锁的方法中,我们需要重写tryAcquire 和tryRelease 方法。
独占式的获取锁和释放锁时,需要在tryAcquire方法中记录到底是哪一个线程获取了锁。一般使用exclusiveOwnerThread字段(setExclusiveOwnerThread方法)记录,在tryRelease 方法释放锁成功之后清楚该字段的值。
acquire流程:
release流程:
根据在上面的源码,我们尝试总结出acquire方法(独占式获取锁)构建同步队列的一般流程为。
首先第一个线程A调用lock方法,此时还没有线程获取锁,那么线程A在acquire的tryAcquire方法中即获得了锁,此时同步队列还没有初始化,head和tail都是null。
此时第二个线程B进来了,由于A已经获取了锁,此时该线程将会被构造成结点添加到队列中,enq方法中,第一次循环时,由于tail为null,因此将会构造一个空结点作为同步队列的头结点和尾结点:
第二次循环时,该结点将会添加到结点尾部,tail指向该结点!
然后在acquireQueued方法中,假设结点自旋没有获得锁,那么在shouldParkAfterFailedAcquire方法中将会设置前驱结点的waitStatus=-1,然后该结点的线程B将会被挂起:
接下来,如果线程C也尝试获取锁,假设没有获取到,那么此时C也将会被挂起:
从这里能够看出来,一个结点的SIGNAL状态(-1)是它的后继子结点给它设置的,那多条线程情况下,最有可能的情况为:
到此acquire一般流程分析完毕!
根据在上面的源码以上面的图为基础,我们尝试总结出release方法(独占式锁释放)的一般流程为:
假如线程A共享资源使用完毕,调用unlock方法,内部调用了release方法,此时先调用tryRelease 释放锁,释放成功之后调用unparkSuccessor方法,设置head结点状态为0,并唤醒head结点的没有取消的后继结点(waitStatus不大于0),这里明显是B线程结点。resize方法到这里其实已经结束了,下面就是被唤醒结点的操作。
调用unpark唤醒线程B之后,线程B在parkAndCheckInterrupt方法中继续执行,首先判断中断状态,记录是因为什么原因被唤醒的,这里不是因为中断而被唤醒,因此返回false,那么acquireQueued的interrupted字段为false。
然后线程B在acquireQueued方法中继续自旋,假设此时B获取到了锁,那么调用setHead方法清除线程记录,并将B结点设置为头结点。这里清除了结点内部的线程记录也没关系,因为在我们实现tryAcquire方法中一般会记录是哪个线程获取了锁。
当最后一个阻塞结点被唤醒,并且线程E获取锁之后,同步队列的结构如下:
当最后一个线程E共享资源使用完毕调用unlock时,在release中释放锁之后,再尝试利用head唤醒后继结点时,判断此时head结点的waitStatus还是等于0,因此不会再调用unparkSuccessor方法。
到此release一般流程分析完毕!
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!