(一)、AQS概述
AbstractQueuedSynchronizer简称AQS,它是java.util.concurrent包中,它提供了一套完整的同步编程框架。我们常用的ReentrantLock、CountDownLatch都是基于AQS实现的。
AQS的实现分为两种形式,一种是独占锁,另一种则是共享锁。
- 独占锁:每次只能有一个线程持有锁。我们比较熟悉的ReentrantLock就是通过独占锁实现互斥性的。
- 共享锁:允许多个线程同时获取锁,并发地访问资源。例如ReentrantReadWriteLock。
在这篇文章中,我们先来分析一下AQS的独占锁机制。
(二)、AQS内部实现
- AQS的实现是底层底层维护了一个先进先出(FIFO)的双向队列,这个队列是基于链表实现的。如果线程竞争锁失败,那么就会进入到这个同步队列中进行等待。当获得锁的线程释放锁之后,会从队列中唤醒一个线程。
- 双向队列是基于Node节点实现的,当线程需要入队列的时候,会将线程的信息封装成一个Node对象,进行入队操作。
- 属性head就标记头节点,属性tail标记尾节点。
static final class Node {
//方式一:标记为共享锁(mode)
static final Node SHARED = new Node();
//方式二:标记为独占锁(mode)
static final Node EXCLUSIVE = null;
//节点从同步队列中取消
static final int CANCELLED = 1;
//后继节点的线程处于等待状态,如果当前节点释放锁,会通知后继节点
static final int SIGNAL = -1;
//当前节点处于等待队列中
static final int CONDITION = -2;
//这个用于共享锁,表示共享式同步状态的传递
static final int PROPAGATE = -3;
//在同步队列中的等待状态
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//加入同步队列的线程引用
volatile Thread thread;
//等待队列中的下一个节点
Node nextWaiter;
//队列中的下一个对象是否为共享式
final boolean isShared() {
return nextWaiter == SHARED;
}
//获得当前节点的前驱节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
//添加等待者
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
//头节点
private transient volatile Node head;
//尾节点
private transient volatile Node tail;
//标记状态
private volatile int state;
(三)、锁的获取
获得独占锁的源头就是从acquire()方法开始的。这个方法中分别调用了三个方法:tryAcquire()、addWaiter()、acquireQueued()。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先调用的是tryAcquire(arg),这个方法只抛出了一个异常,因为它的具体实现是交给子类去完成的。这个方法的主要功能是:尝试获得锁,进入临界区。 我们只要知道这个即可。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
根据acquire()方法中,tryAcquire()方法获取锁失败,就会进入addWaiter()方法,这个方法的主要功能是:将线程封装成Node节点并加入到同步队列中。 注意acquire方法中传入的参数Node.EXCLUSIVE,代表使用独占锁。
private Node addWaiter(Node mode) {
//将线程的信息封装成Node节点
Node node = new Node(Thread.currentThread(), mode);
// 获得队尾节点
Node pred = tail;
//如果队尾节点不为null
if (pred != null) {
//设置新节点的前驱节点是队尾节点
node.prev = pred;
//通过CAS操作将新节点设置为队尾节点
if (compareAndSetTail(pred, node)) {
//如果成功,就将队尾节点的后继节点设置为新节点
pred.next = node;
return node;
}
}
//如果这个同步队列是空队列或者CAS失败,那就调用enq()方法
enq(node);
return node;
}
通过上面的代码我们知道,如果这个同步队列是空队列或者CAS失败,那就调用enq()方法。
private Node enq(final Node node) {
//死循环,相当于自旋操作
for (;;) {
//获取队列的尾节点
Node t = tail;
//如果是个空队列
if (t == null) { // Must initialize
//CAS操作创建一个新节点为头节点
if (compareAndSetHead(new Node()))
//设置为尾节点
tail = head;
} else {
//设置新节点的前驱节点为尾节点
node.prev = t;
//尝试将node节点设置为尾节点
if (compareAndSetTail(t, node)) {
//设置t节点的后继节点为node
t.next = node;
return t;
}
}
}
}
然后到这里为止我们就走完了addWaiter,然后又会回到acquire()方法,调用acquireQueued(),这个方法的功能是:尝试成为头节点,也就是尝试获得锁。
final boolean acquireQueued(final Node node, int arg) {
//标记是否失败
boolean failed = true;
try {
//标记是否被中断
boolean interrupted = false;
//死循环,自旋操作
for (;;) {
//获得node节点的前驱节点
final Node p = node.predecessor();
//判断p是否是头节点,当p是头节点,p才有可能参加锁竞争
//如果p是头节点了,那就会调用tryAcquire()方法尝试获得锁
if (p == head && tryAcquire(arg)) {
//获得锁成功,将node设置为头节点
setHead(node);
//将p节点出队列
p.next = null; // help GC
//标记为成功
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire()根据节点的waitStatus()来决定是否挂起线程
//parkAndCheckInterrupt()是将线程挂起的方法
//这两个方法后面都会解释
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//抛出异常
if (failed)
//进行出队列操作。
cancelAcquire(node);
}
}
分析完上面的代码,我们来进入shouldParkAfterFailedAcquire(p, node)方法,这个方法的主要功能是:根据节点的waitStatus()来决定是否挂起线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的waitStatus
int ws = pred.waitStatus;
//如果ws等于signal,这个在前面提到过,如果为signal,那么就标记为出队列时会唤醒下一个线程
if (ws == Node.SIGNAL)
//那就不用修改,直接返回true
return true;
if (ws > 0) {
//如果状态值大于0,那么表示该节点为取消状态
do {
//将pred的前驱节点设置为node的前驱节点,相当于pred出队列
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//将pred的后继节点设置为node
pred.next = node;
} else {
//如果为其他状态,就用CAS操作转换为SIGNAL状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果上面的代码返回true,也就是前驱节点被标记为SIGNAL,这样当前驱节点释放锁的时候,会去通知当前节点node。这样node节点就可以进行挂起。 parkAndCheckInterrupt()方法的主要功能是:将线程挂起,等待其他线程唤醒。
private final boolean parkAndCheckInterrupt() {
//将线程挂起
LockSupport.park(this);
//返回中断状态
return Thread.interrupted();
}
这里对LockSupport的park的方法解释一下:
LockSupport
LockSupport类是Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:
public native void unpark(Thread jthread);
public native void park(boolean isAbsolute, long time);
unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。
permit相当于0/1的开关,默认是0,调用一次unpark就加1变成了1.调用一次park会消费permit,又会变成0。 如果再调用一次park会阻塞,因为permit已经是0了。直到permit变成1.这时调用unpark会把permit设置为1.每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark不会累积
(三)、锁的获取方法总结
锁的获取,其实就是将获取锁的线程放在同步队列的首部,然后其他等待的线程就通过LockSupport的park()方法进行阻塞。
acquire方法:
- 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false
- 如果tryAcquire失败,先通过addWaiter方法将当前线程的信息封装成Node节点添加到AQS队列尾部
- acquireQueued,将Node作为参数,通过自旋去尝试获取锁。
addWaiter方法:
- 将当前线程的信息封装成Node
- 判断当前链表中的tail节点是否为空,如果不为空,则通过cas操作把当前线程的node添加到AQS队列。
- 如果为空或者cas失败,调用enq方法将节点添加到AQS队列
enq方法:
- 获得尾部节点tail,并赋值给t
- 如果尾部节点为null,表示空队列,就新建一个头节点,并设置为尾节点
- 如果不是空队列,将node的前驱节点设置为尾节点,尝试用CAS添加为尾节点的后继节点
acquireQueued方法:
- 获取当前节点的前驱节点p
- 如果前驱节点p是头节点,然后就尝试去获得锁
- 如果获得锁成功,就将当前节点node设置为头节点
- 如果获得锁失败,调用shouldParkAfterFailedAcquire看是否能够挂起线程
- 如果能够挂起线程,通过parkAndCheckInterrupt对线程进行阻塞
shouldParkAfterFailedAcquire方法:
- 获取当前节点的prev节点
- 如果prev节点的状态是SINGINAL,那么就无需操作,直接返回true
- 如果状态大于0,那就是CANCEL状态,就将prev节点出队列,并重新整理链表
- 最后,使用CAS操作尝试将前驱节点的状态设置为SIGNAL
parkAndCheckInterrupt方法:
- 调用LockSupport的park()方法进行阻塞操作。
(四)、可中断式获取锁
可中断式获取锁和普通获取锁的区别就是在尝试获取锁的过程中,可以响应中断,从而停止尝试获取锁。
下面来看源代码,方法是acquireInterruptibly()的功能是:可以响应中断地尝试获取锁。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取锁
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
上面的源码中调用了doAcquireInterruptibly方法,它的主要功能是:响应中断地尝试获取锁。
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//将节点添加到同步队列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
//死循环,进行自旋
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())
//线程中断,抛出异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个方法和之前的doAcquire实现几乎是一样的,**唯一的区别就是parkAndCheckInterrupt返回true时即线程阻塞时该线程被中断,代码抛出被中断异常。**这里就不做过多的解释了。
(五)、超时等待获取锁
这个方法主要的作用是限制线程等待锁的时间,如果超过时间就直接返回,不再进行等待。下面来看一下源码:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//尝试获得锁
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
方法中调用了doAcquireNanos()方法,这个方法的主要功能是:限定等待锁释放的时间,如果超时就直接返回。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//判断是否合法
if (nanosTimeout <= 0L)
return false;
//计算最晚结束时间
final long deadline = System.nanoTime() + nanosTimeout;
//将线程添加进队列
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
//死循环,自旋操作
for (;;) {
//获得当前节点的前驱节点
final Node p = node.predecessor();
//判断p是不是头节点,并且尝试获得锁
if (p == head && tryAcquire(arg)) {
//获得锁成功,设置为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//计算剩余等待时间
nanosTimeout = deadline - System.nanoTime();
//如果超时了,直接返回false,获取失败
if (nanosTimeout <= 0L)
return false;
//shouldParkAfterFailedAcquire判断是否能够阻塞
//spinForTimeoutThreadhold是最小自旋时间
//调用LockSupport.parkNanos进行限时阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
主要逻辑如下图:
(六)、锁的释放
锁的释放涉及的方法是release(),下面我们来详细分析一下源码
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
//如果成功,获取头节点并赋值给h
Node h = head;
//如果头节点不为null
if (h != null && h.waitStatus != 0)
//唤醒下一个线程
unparkSuccessor(h);
return true;
}
return false;
}
上面的tryRelease中只是简单地抛出了一个异常,这个方法是需要子类进行重写的。它的主要功能是:尝试释放锁
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
然后我们接着来看unparkSuccessor()这个方法,它的主要功能是唤醒下一个线程。
private void unparkSuccessor(Node node) {
//获取节点的状态
int ws = node.waitStatus;
//如果状态小于0,使用CAS修改状态为0
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)
s = t;
}
//如果s节点存在
if (s != null)
//唤醒线程
LockSupport.unpark(s.thread);
}
(七)、总结
通过以上分析AQS的独占锁,我们可以清楚地了解到他是如果通过acquire方法来保证只有一个线程能够执行同步代码块的,那就是LockSupport的park()方法,我们也更加清晰地了解了底层数据结构。之后我还会继续写关于共享锁的分析,其实原理也差不多。谢谢大家的阅读!