上篇文章简述了 AQS 的提供的几个支持,结果忘记写 AQS 内置的队列,在这里和大家说一句–抱歉,后面笔者会对博客进行重写添加。那今天,我们来学习 AQS 是如何实现独占锁功能的。
说到独占锁,我们就会想到 synchronized 关键字,既可以锁方法,也可以使用锁住某一块代码,简直就是个万能锁。当然今天我们肯定不是讲它,而是讲 AQS 提供的独占功能的类 – ReentrantLock。我们先来看看上篇文章的两个锁的对比图:
锁 | synchronizer | Lock |
---|---|---|
超时中断 | × | √ |
独占 | √ | √ |
共享 | × | √ |
等待队列 | √ | √ |
看完上图,我们可以知道,ReentrantLock 在 synchronizer 基础上添加了的更多的功能。现在我们来看看 ReentrantLock 是如何实现这些功能的。
先看看该类的类图:
我们从类图可以知道,ReentrantLock 实现了 Lock 接口,并且在内部中有三个内部类,内部类继承 AQS 类,其他暂且先忽略。现在我们先看看 Lock 接口抽象的方法:
public interface Lock {
/**
* 拿锁(如果没拿到会阻塞当前线程)
*/
void lock();
/**
* 拿可中断的锁
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试去拿锁(拿成功返回 true )
*/
boolean tryLock();
/**
* 尝试去拿锁(有超时时间,超时没拿到就中断)
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
/**
* 线程阻塞/唤醒(后面篇章讲解)
*/
Condition newCondition();
}
ReentrantLock 锁抽象:
ReentrantLock 中有三个内部类,其中 Sync 类是锁抽象,用于实现公平锁和非公平锁。先简单看看 Sync 内部类:
abstract static class Sync extends AbstractQueuedSynchronizer {
/**
* 抽象方法,用于区分公平锁和非公平锁具体实现
*/
abstract void lock();
/**
*
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
/**
* 释放锁
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
/**
* 阻塞/唤醒,后续篇章介绍
*/
final ConditionObject newCondition() {
return new ConditionObject();
}
/**
* 判断锁状态,0-为未上锁,1-被占用
*/
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
/**
* 判断锁状态,0-为未上锁,1-被占用
*/
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
/**
* 判断锁状态,0-为未上锁,1-被占用
*/
final boolean isLocked() {
return getState() != 0;
}
//省略下面代码。。。
}
ReentrantLock 公平策略:
ReentrantLock 中公平策略和非公平策略的实现是不一样的,先看看 FairSync 类的具体方法:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//acquire 是父类方法,内部户调用 tryAcquire 方法拿锁
final void lock() {
acquire(1);
}
/**
* 公平策略的 tryAcquire 方法,和非公平锁不同
*/
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前锁状态
int c = getState();
//0-说明没被占用
if (c == 0) {
//判断是否是第一个入队的线程
if (!hasQueuedPredecessors() &&
// CAS 修改当前锁状态
compareAndSetState(0, acquires)) {
//将当前线程保留起来
setExclusiveOwnerThread(current);
return true;
}
}
//判断是不是同一线程,是的话允许重入
else if (current == getExclusiveOwnerThread()) {
//同一线程多次重入会累加
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//修改锁状态
setState(nextc);
return true;
}
return false;
}
}
在公平策略模式下,当我们调用 lock.lock()加锁时,首先调用的是:
public void lock() {
sync.lock();
}
由于使用的是公平模式,在 new 的时候已经构建好了,接下来就是调用 FairSync 内的 lock 方法,如下:
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
//省略部分代码
}
当我们点击 acquire 方法时,这个方法在 AQS 类中,我们来看看该方法实现:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
//这里涉及到入队了,链表 Node 的方法,暂不解释
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先 tryAcquire 方法就是我们公平锁中实现的方法。
tryAcquire 方法解析:
static final class FairSync extends Sync {
/**
* 公平策略的 tryAcquire 方法,和非公平锁不同
*/
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前锁状态
int c = getState();
//0-说明没被占用
if (c == 0) {
//判断是否是第一个入队的线程
if (!hasQueuedPredecessors() &&
// CAS 修改当前锁状态
compareAndSetState(0, acquires)) {
//将当前线程保留起来
setExclusiveOwnerThread(current);
return true;
}
}
//判断是不是同一线程,是的话允许重入
else if (current == getExclusiveOwnerThread()) {
//同一线程多次重入会累加
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//修改锁状态
setState(nextc);
return true;
}
return false;
}
}
tryAcquire 方法在假设是有线程占用了的话,肯定返回的是 false,接着调用 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 方法,先看看 addWaiter 方法是怎么样的!
addWaiter 方法解析:
private Node addWaiter(Node mode) {
//包装一个当前节点,将线程和将当前Node节点的下一个等待节点指向传入节点
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;
}
enq 方法解析:
private Node enq(final Node node) {
for (;;) {
//拿到尾节点
Node t = tail;
//为空,构建一个空节点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//将前置节点指向尾节点
node.prev = t;
//CAS 设置尾节点
if (compareAndSetTail(t, node)) {
//将前置的节点的下一个节点指向当前节点。
t.next = node;
return t;
}
}
}
}
上面理解起来可能比较难,接下来我们用图来解析是如何进行的:
现在有三条线程,分别为 A B 。这时,线程 A 开始开始来拿锁,拿到锁后,就占住。线程 A 占住锁后,队列还是处于空状态。
这时线程 B 来了,还是会去尝试拿锁。发现锁已经被占用了,就返回的是 false ,接下来就尝试去进入队列,也是就是调用 enq 入队方法。这时,我们发现队列是空的,CAS 尝试构建一个对象,构建成功后将头尾指针都指向这个空节点。
这时,头结点和尾节点后指向同一个,结果如下图所示:
都走完后,结束第一轮循环,走下一个循环。这时我们发现尾节点已经不是空的了,走 else 分支:
上面代码很清晰的可以看出,我们传入的 node 对象的前置节点指向 尾指针指向的对象,之后 CAS 尝试设置尾节点,成功的话,尾指针指向的节点的 next 指向我们传入的 node 节点。
开始是这样的:
之后,将 pre 指向尾节点(也可以说头节点,因为暂时头尾都指向同一个)
接下来,这里可能会出现并发情况,会尝试 CAS 设置尾指针指向。如果 CAS 成功设置,那么会将头接的 next 指向我们的传入的节点,结果如下图:
到此,我们线程 B 已经入队成功了。但是这仅仅完成了一半,接下来才是重点。如果我们还记得 acquire 方法的话,最终会调用 acquireQueued() 这个方法。该方法也是在 AQS 类中做了具体实现。
acquireQueued 解析:
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);
//将原来的头节点的 next 指向置空
p.next = null; // help GC
//这个是判断是否让线程继续阻塞
failed = false;
return interrupted;
}
//判断线程是否应该阻塞,parkAndCheckInterrupt 是对当前线程进行阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果线程被中断了,将 interrupted 设置为 true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
**shouldParkAfterFailedAcquire 方法解析: **
上面代码中,shouldParkAfterFailedAcquire 方法是判断该线程是否应该被阻塞,而 parkAndCheckInterrupt 则是对线程进行阻塞,下面我们看看源码实现:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前置节点状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
//大于 0, 表示该节点已经无效了,需要移除,
//这时,不断循环找到没有失效的节点
//将该节点的 next 指向自己
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//若 pre 节点状态不正确,都将其变成 SIGNAL 阻塞状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这里要说一下 Node 的几个状态:
状态 | 值 | 描述 |
---|---|---|
CANCELLED | 1 | 该状态表示线程可能被中断 |
SIGNAL | -1 | 表示当前线程需要需要被阻塞,处于这个状态下才可以安全的阻塞 |
CONDITION | -2 | 暂时用不到,这时线程间阻塞唤醒使用 |
PROPAGATE | -3 | 与共享模式相关,暂时不理 |
上面代码中,只有处于 SIGNAL 状态时,才可以安全的将其阻塞。大于 0 的情况只有线程已经被中断,所以需要不断循环找前置没有失效的节点。
parkAndCheckInterrupt 方法解析:
parkAndCheckInterrupt 方法很简单,看名字就知道这是阻塞和检查线程是否被中断。LockSupport 该类会在后续篇章讲解。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
讲到这里,线程 B 入队到阻塞的情况基本讲解完毕。下面我们开始看看线程 A 执行完毕释放锁后,程序是如何继续运行的。
正常来说,我们释放锁时,会调用 Lock,unlock()方法来进行释放,unlock 内部调用 release 方法,代码如下:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//tryRelease 在 Sync 类中,不在 AQS 类里
//重写了父类 AQS tryRelease
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒阻塞的线程
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease 方法解析:
protected final boolean tryRelease(int releases) {
// releases 传的是 1 ,getState 中 1-表示占用
int c = getState() - releases;
//判断是否是当前线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//等于 0 说明可以释放
//置空线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//改变锁状态
setState(c);
return free;
}
unparkSuccessor 方法解析:
该方法在 AQS 类中,是 AQS 一个很重要的类,用于唤醒线程。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//修改线程状态
compareAndSetWaitStatus(node, ws, 0);
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;
}
//唤醒线程
if (s != null)
LockSupport.unpark(s.thread);
}
解析较为模糊,我们用图的方式来解析:
当我们线程 B 入队并阻塞时,waitState 的状态值是 -1。
接着,线程 B 被唤醒后继续往下走,会走如下代码:
线程 B 尝试去拿到锁,如果成功拿到锁之后将头尾指针指向同一个节点。(仅仅两个线程会出现这种情况,多个的话,最后一个节点才是尾指针指向。)
后面线程 B 释放锁时,会将 waitStatus 设置为 0,后面都一样了。当然,假设阻塞队列中有最后一个线程阻塞唤醒时,unparkSuccessor 方法什么都不做。
ReentrantLock 非公平策略:
非公平锁的实现和公平锁的实现不一样,接下来我们先看看非公平锁的类:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//尝试上锁
final void lock() {
//未被占用,尝试 CAS 修改状态并保存当前线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//占用则自旋尝试取锁
acquire(1);
}
//重写的方法,非公平实现
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
当我们调用 Lock.lock()时,首先是判断锁是否被占用,如未占用,会 CAS 尝试拿到锁,被占用的情况下回调用 acquire 方法,但是该方法和公平锁一样,都是在父类 AQS 中,最后调用的还是 tryAcquire 方法,就是上面代码中的 tryAcquire 方法。下面是非公平锁的调用实现:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//拿到当前线程
int c = getState();
if (c == 0) {
//CAS 尝试修改
if (compareAndSetState(0, acquires)) {
//保存当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//拿到当前线程,判断是否为同一线程,是的话允许重入。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
其他入队,阻塞实现和公平锁一致,就不重复写了。下面要说 AQS 的中断机制。
AQS 的锁中断机制:
如果我们有业务是允许中途中断的,那么我们需要使用可中断锁了,可中断锁使用很简单,就是调用如下方法即可。
lock.lockInterruptibly();
接下来我们来看看 lockInterruptibly 的代码,如下可以知道内部调用 acquireInterruptibly 方法,但是这个方法是在父类也就是 AQS 中做了实现,我们来看看父类实现的这个方法。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
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);
}
}
差不多讲完了将 ReentrantLock 源码解析完了,剩下一个限时去拿锁的方式了,下面我们继续来看看。
AQS 锁的限时等待机制:
AQS 里有对限时等待拿锁的支持,Lock 接口也提供了该方法的抽象,有尝试去拿锁的,也有一定时间内不断尝试去拿锁的,方法如下:
//尝试去拿锁
lock.tryLock();
//10 秒内不断尝试去拿锁
lock.tryLock(10,TimeUnit.SECONDS);
限时等待方法实现也不难,当我们调用有时间的 tryLock 时,内部调用代码如下:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
tryAcquireNanos 方法解析:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//判断线程是否被中断
if (Thread.interrupted())
throw new InterruptedException();
//尝试拿锁,不成功则限时去尝试拿
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
tryAcquire 方法就不写了,写到烂了,我们来看看 doAcquireNanos 这个方法:
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();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//还有下面这两个判断
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
//时间范围内,且状态修改为 SIGNAL,则阻塞线程
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
//如果成功就阻塞线程
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
好了,到这里基本就写完了 AQS 的独占锁详解,后面 AQS 对中断机制与限时等待机制的支持都是基于最开始的 doAcquireShared 进行部分修改。相关实现我们看代码可以知道都是 CAS 操作来保证,就不继续讲解了。
补充:
显示锁和内置锁的效率对比图
(以下图片来源于网络)
最后:
既然 AQS 对锁支持的那么好,而且按照上图中效率对比,AQS 对锁的支持都比 synchronized 好,那 synchronized 应该可以淘汰吧?答案是不一定,《深入理解 java 虚拟机》 中作者也说明,synchronized 锁有很大的优化空间,而且上图的图标是基于 JDK1.5 的。在后续的版本中,对 synchronized 做了极大的优化,优化后的内置锁效率上也是很高了。
在日常开发中,synchronized 关键字使用更方便、更广泛。即使 AQS 提供的锁支持比内置锁多了更多的功能。如果是简单的锁变量之类的,优先还是使用内置锁,除非有特别的业务要可以对这个资源进行中断,超时,那就使用显示锁吧。
吐血,这一篇居然写了 7 个小时,真是没想到,没想到呀!不过很庆幸终于写完了!