前言
最近非常着迷阅读源码,周末闲来无事,又把AQS【AbstractQueuedSynchronizer】过了一遍。
网上有很多这方面的文章,为什么还要自己去写呢?一是为了加深记忆,二是为了方便日后查看,三是锻炼归纳总结的能力。有缘的读者可以大致看一下。或许有些帮助。
对于初学者来说可能并不熟悉AQS,它是java.util.concurrent包下的工具类,日常开发中,很少有人去使用它,但是ReentrantLock、CountDownLatch都用过吧,至少听过说吧。它们都是基于AQS实现的。
为了能更深入的理解锁的概念,我觉得有必要深入了解这些工具类。
AQS概览
首先对AQS有个总体认识,它都有什么功能,如何作为抽象类给其他类提供帮助。
AQS提供两种锁的解决方案:
- 独占锁:在锁的区域内(可以理解为日常开发中sychronized包裹的区域,或者说lock--unlock的区域),只允许一个线程进入。ReentrantLock就是独占锁。
- 共享锁:在锁的区域内,允许多个线程进入。CountDownLatch、Semaphore都是共享锁。
那么如何利用AQS来实现独占锁/共享锁呢?
大体过程:新建一个类TestLock,然后在TestLock内部创建一个内部类TestSync extends AbstractQueuedSynchronizer,TestSync 重写AQS的tryAcquire/tryAcquireShared和tryRelease/tryReleaseShared。接着在TestLock类中创建testlock方法写一些获取锁前的判断逻辑,最后调用AQS的acquire/acquireShared方法来获取锁。创建testunlock方法写一些锁的释放逻辑最后调用AQS的release/releaseShared方法来释放锁。
其中tryAcquire和tryAcquireShared分别代表获取独占锁的方法和获取共享锁的方法。release/releaseShared代表释放独占锁/释放共享锁的方法。
AQS可以看作是一个锁引擎,我们可以利用它来实现定制锁。
说了这些可能该懵逼的还是一脸懵逼,下边通过解读ReentrantLock,来一步步分析AQS是如何帮助ReentrantLock来实现独占锁的。
ReentrantLock分析
先看下它的类图
日常使用ReentrantLock的流程。
//默认构造方法是非公平锁
ReentrantLock lock=new ReentrantLock();
//加锁,用于应对锁区域的高并发,只能有一个线程进入锁区域,其他线程等待
lock.lock();
//执行业务代码。。。。
//。。。。
//释放锁
lock.unlock();
上边第一句代码就是创建ReentrantLock对象,调用默认构造方法,默认构造方法内部创建一个NonfairSync非公平锁,NonfairSync是ReentrantLock的内部类,它最终继承自AQS,重写了tryAcquire和tryRelease等方法。所以概括起来,第一句就是创建了一个非公平锁对象。
第二句就是加锁了,点进去看一下源码,最终调用的NonfairSync的lock方法:
final void lock() {
//通过CAS操作,假如AQS中state状态为0,则设置state为1
//如果设置成功,则代表当前线程成功获取到锁
//如果设置state失败,则调用AQS的acquire方法去获取锁。
//在这里也体现了非公平锁的特性,就是新来一个线程要获取锁,首先会尝试CAS获取锁,这样就失去了公平性,它可以同正在排队的线程一同竞争获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
跟我上边说的流程一样,写一些锁的逻辑最后调用AQS的acquire/acquireShared方法来获取锁。具体逻辑参考代码中的注释,到这里引出了AQS中state成员变量。它的作用可以理解为锁的状态,假如为0,则代表当前没有锁,线程可直接进入锁区域,假如state大于0,代表当前存在锁竞争,成功获取到锁的线程会进入锁区域,其他线程需要排队等候。
在lock方法中,假如当前state大于0,存在锁竞争,则会调用AQS的acquire方法,代码如下:
public final void acquire(int arg) {
//简单的一个if判断,其实是执行了三个方法。
//首先调用tryAcquire方法,tryAcquire方法上边说过,是实现类自己实现的获取锁方法,
//假如获取锁成功,则直接返回。
//假如获取锁失败,会调用addWaiter(Node.EXCLUSIVE)方法,该方法将当前线程添加到AQS的等待队列中,
//接着调用acquireQueued方法,该方法内是一个死循环,正常情况下循环两次后线程会进入等待阻塞状态
//直到其他线程唤醒它,继续执行死循环,直到满足条件跳出循环。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果在线程等待过程中,线程状态被设置为interupt状态,会进入if中,调用线程的interupt方法,调用该方法后,会抛出异常。
selfInterrupt();
}
acquire方法是AQS中比较重要的方法,详细的流程参见上边的注释。针对代码中调用的几个方法,我们再深入分析一下,先看下
tryAcquire方法,上边说了该方法是实现类实现的,那就看下ReentrantLock中非公平锁NonfairSync内是如何实现的该方法:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
我们发现内部调用的nonfairTryAcquire方法,看下nonfairTryAcquire方法:
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取AQS的state
int c = getState();
//如果state为0,代表没有加锁,尝试使用CAS来获取锁,就是通过修改state来获取锁
if (c == 0) {
//获取设置state成功,说明获取到锁,并设置当前线程拥有该锁。
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果state不为0,说明已经有线程获取到锁,如果获取到锁的线程是当前线程,则直接修改state的状态。
//这种实现代表ReentrantLock是可重入锁。
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;
}
概括tryAcquire方法的作用,成功获取锁则返回true,获取锁失败,则返回false。
接着回到acquire方法,看完了tryAcquire方法,看下addWaiter方法,addWaiter方式是在tryAcquire方法获取锁失败后执行的:
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
//创建AQS中双向链表节点,该节点代表当前线程,该节点的下一个节点为mode,mode值是Node.EXCLUSIVE=null,
Node node = new Node(Thread.currentThread(), mode);
//获取链表的尾节点
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//如果尾节点不为null,设置当前节点为新的为尾结点,
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果设置失败,则进入死循环,一直尝试将当前节点添加到队列,直到添加成功
enq(node);
//返回当前节点。
return node;
}
addWaiter方法中注意enq(node)方法,当执行到这个方法时,说明队列是空的。看下方法内是如何做的:
private Node enq(final Node node) {
//死循环
for (;;) {
//获取队列尾节点
Node t = tail;
//如果为节点为null,则初始化队列。
if (t == null) { // Must initialize
//初始化队列,头节点为new Node不包含线程,如果创建队列成功,则头尾节点都为new Node
if (compareAndSetHead(new Node()))
tail = head;
} else {
//第二次循环,因为已经初始化队列,尾节点不为null,走else
//设置当前线程node为新的尾结点,它的前节点就是new Node
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
接着回到acquire方法,看完了tryAcquire和addWaiter方法,看下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);
p.next = null; // help GC
failed = false;
return interrupted;
}
//这个if判断包含两个方法,首先执行shouldParkAfterFailedAcquire方法,如果返回true则继续执行parkAndCheckInterrupt方法
//shouldParkAfterFailedAcquire方法是判断前节点的状态,假如为Node.SIGNAL则返回true。
//parkAndCheckInterrupt方法阻塞当前线程,直到其他线程唤醒当前线程,代码才会继续往下执行。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//假如当前线程在获取锁之前出现异常,则取消当前线程获取锁
if (failed)
cancelAcquire(node);
}
}
针对acquireQueued方法中shouldParkAfterFailedAcquire方法,看下内部是如何实现的:
//shouldParkAfterFailedAcquire方法是判断前节点的状态,假如为Node.SIGNAL则返回true。
//如果前节点为Node.CANCELLED。则跳过这个节点找前节点的前节点,直到目标节点不是Node.CANCELLED状态。
//并设置目标节点为当前节点的前节点(相当于把CANCELED节点从队列中移除了)。返回false。
//等到下次循环进入该方法,当前节点的前节点保证不是CENCELED状态了,接着判断前节点是否为Node.SIGNAL,
//如果不是,则设置前节点状态为Node.SIGNAL。
//等到下次循环进入该方法,前节点一定为Node.SIGNAL,返回true,执行parkAndCheckInterrupt方法。
//所以该方法,最快的话,一次循环返回true,最慢3次循环返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前节点的状态。状态如果没有赋值,默认为0.
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
对于Node的状态,这里多说一嘴,一共4个状态:
/** waitStatus value to indicate thread has cancelled */
//当节点状态为CANCELLED时,会被程序从队列中删除
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
//当节点状态为SIGNAL时,如果程序从队列中唤醒
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
//当前文章用不到该状态,
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
//当前文章用不到该状态,
static final int PROPAGATE = -3;
shouldParkAfterFailedAcquire方法中,状态>0代表是Node.CANCELLED。如果状态没有被赋值过,则默认为0.
以上基本对ReentrantLock类的加锁过程分析了一遍。
概括一下流程:
- 调用lock方法获取锁,
- 获取锁成功(获取锁成功还是失败由谁判定?=》tryAcquire方法)则执行锁内部代码,获取锁失败则进入等待队列。
参考流程图
说完了获取的流程,接着说下释放锁的流程,看下unlock的代码:
public void unlock() {
sync.release(1);
}
代码很简单,直接调用AQS的release方法:
public final boolean release(int arg) {
//if判断,调用实现类tryRelease方法,判断是否能成功释放锁,如果释放成功,
//进入if,如果队列中头节点不为null,并且状态不为默认值0,则调用唤醒线程方法。将阻塞的线程唤醒继续执行。
//至于唤醒的是哪个线程看下边对unparkSuccessor的分析
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) {
//定义变量C,为释放锁后的state值。为state-1
int c = getState() - releases;
//如果当前线程,不是拥有锁的线程,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果c为0,代表没有锁了,设置锁中对应的线程变量为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//修改state
setState(c);
//只有c为0时才会返回true。
return free;
}
接着看下unparkSuccessor方法:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
//获取成员变量node状态
int ws = node.waitStatus;
//如果状态小于0,通过CAS设置其等于0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//获取头节点的下一个节点
Node s = node.next;
//如果头节点下节点为null或者下节点的状态为CANCELED,则从尾部开始尝试遍历,找到状态小于0的节点
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);
}
在unparkSuccessor方法中,到底唤醒的是队列中的哪个节点?
可以概括为:先判断头节点的下一个节点是否为null或者状态为CANCELED,如果是,则从队列尾开始向头方向找,直到找到一个不为CANCELED节点作为唤醒节点,如果不满足条件,则唤醒头节点的下一个节点。当然唤醒前需要做判断,必须节点不为null才会被唤醒。
以上就是unlock方法的执行流程。
总结
以上只是对AQS独占锁的分析,共享锁的实现跟独占锁略有区别,只要摸清了一种锁的实现方式,另外一种就很容易看懂了。
通过对ReentrantLock分析,从中学到了AQS引擎的结构和它的核心方法。整个流程其实很简单,但是每一步都比较繁琐。
因为是针对多线程,方法中有许多变量都是公用的,如state,head,tail等。当多个线程并发访问时,从头捋清楚代码是如何执行的还是比较困难的。这篇文章,也只是按照我的思路来考虑并发,许多细节可能未考虑周到。但整体思路是没有问题的。不妨碍学习AQS的精髓。
由于多线程的干扰,调试也比较困难,所以对于锁,能满足需求的尽量使用官方的工具类,自己实现难免会考虑不周,出现各种bug。