AQS
AQS指的是一个抽象类: AbstractQueuedSynchornizer(抽象队列同步器), 位于juc包下, 派生自AbstractOwnableSynchornizer(抽象可拥有同步器), 看看类的继承关系:
我们发现, 在CountDownLatch, ThreadPoolExecutor, ReentrantLock, ReentrantReadWriteLock, Semaphore中, 都有一个/多个继承自AQS的内部类, 说明AQS是一个提供公共的队列同步方法的类, 其它继承它的类只需要实现其提供的部分抽象方法即可完成需求.
借助ReentrantLock探究AQS原理
自己实现一个Lock
ReentrantLock加锁的本质是对一个锁对象中封装的一个int类型的state属性进行cas操作, 操作成功就代表加锁成功, 其它的线程将无法对该state属性进行操作, 然后阻塞, 就达到了同步的目的. 下面我们通过继承AQS, 自己实现一个简易的锁, 看看能不能做到对++操作的原子性保证.
public class MyLockTest {
static int count = 1;
static MyLock lock = new MyLock();
static void increment() {
lock.acquire(1);
count++;
lock.release(1);
}
static void decrement() {
lock.acquire(1);
count--;
lock.release(1);
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
decrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
static class MyLock extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, arg)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(getState() - arg);
return true;
}
}
}
继承AQS之后, 重写其tryAcquire和tryRelease方法
在tryAcquire方法中, 先使用cas修改state, 改为1(arg传入为1), 如果这个cas成功, 就设置独占线程为当前线程, 并且返回true, 如果cas失败, 则返回false.
在tryRelease方法中, 直接设置独占线程为null, 再设置状态到0, 直接返回true.
运行代码, 发现最终count还是1, 说明我们自己实现的这个锁起作用了.
通过上面的代码, 我们可以看出来, 我们实际上只重写了2个最基本的方法, 但是AQS却帮我们完成了加锁解锁的操作, 说明这些操作都在AQS中封装好了, 由我们自己决定修改状态的逻辑.
在使用ReentrantLock的时候, 我们并不是按照上述代码的例子, 而是先使用lock方法, 再在try-finally结构中的finally块中使用unlock方法, 这样一来, 就能实现跟synchronized一样的效果.
在synchronized章节中, 我介绍了MESA模型, ReentrantLock也是参考了这个模型设计的, 因此ReentrantLock在使用上和实现上都与synchronized有非常多的相似之处. 使用上就不多赘述, 本文主要介绍ReentrantLock的原理, 也就是AQS的原理.
在使用ReentrantLock的时候, 先使用lock方法, 会发现不论多少个线程去使用同一个ReentrantLock对象的lock方法, 最终只会有一个线程能得到返回, 其它线程都将会阻塞在lock方法的过程中. 下面我们就看看lock方法是如何实现这一个需求的.
lock方法
public void lock() {
sync.lock();
}
lock方法调用了sync对象的acquire方法, 传入参数为1, 这个sync类型是Sync, 是ReentrantLock中的一个内部抽象类, 它继承自AQS, 而在ReentrantLock中, 对Sync抽象类还有2个子类, 分别是FairSync(公平Sync)和NonFairSync(非公平Sync), 我们知道ReentrantLock是支持公平与非公平的, 具体实现就是在内部的sync属性赋值的时候, new出来的是FairSync或者NonFairSync.
我们在new ReentrantLock的时候, 就必须决定我们是想要公平锁还是非公平锁
// NonFairSync和FairSync都派生自ReentrantLock内部抽象类Sync, 而Sync派生自AQS
// 我们在new ReentrantLock的时候, 就必须决定我们是想要公平锁还是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
acquire方法是AQS已经帮我们实现好的, 如下粘贴的代码是经过美化之后的, 实际上这个方法只有2行, 但是却非常地难. 因为可读性很差. 在if条件的短路与中执行了非常多的操作, 涉及到多种cas, 线程park, while循环等. 在探究这个代码的时候, 首先得明确if中的短路与的执行过程.
代码先执行tryAcquire, 如果tryAcquire返回true, 由于短路与的缘故, 短路与后面的判断根本就不会去走了, if中的条件就是false, 那么整个acquire方法就走完了, 也就意味着外面的lock()方法可以继续往下走了, 换句话说就是加锁成功了.
如果tryAcquire返回false, 那么将会先执行addWaiter, 然后再将addWaiter的返回值, 以及arg=1传到acquireQueued方法中. 至于acquireQueued方法, 将会是研究的重点.
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
tryAcquire方法
在开篇的例子中, 我们知道tryAcquire是一个抽象方法, 提供给派生类使用的:
类名: java.util.concurrent.locks.AbstractQueuedSynchronizer
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
那么对于ReentrantLock中的sync属性的tryAcquire方法, 就将会有两种实现, 一种是公平的, 一种是非公平的. 我们知道, 对于非公平锁, 实际上就是新来的线程不会参与排队, 直接进行抢锁, 那么我们先来看看非公平的tryAcquire是如何实现的.
类名: java.util.concurrent.locks.ReentrantLock.Sync
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;
}
NonFairSync的tryAcquire调用了父类的nonfairTryAcquire方法. 执行的步骤大概如下:
-
拿到当前线程, 记为current
-
拿到state属性, 记为c
-
判断c是否为0, 如果是0, 代表前面拿state的时候, 还没有其它线程加锁, 因此使用cas, 因为这时候会有并发问题, 如果这时候某个线程cas成功了, 就代表拿到锁了, 将独占线程设置为当前线程current, 然后返回true. 因为ReentrantLock是一个独占锁, 就是说同一时刻只能有一个线程持有锁.
-
如果c不是0, 也就是说前面拿到c的时候, 已经有线程持有了锁, 但是这里会判断当前独占此锁的线程是不是就是当前线程, 如果是当前线程, 将状态+1, 再设置给state, 并且返回true. 这什么意思呢? 很明显, 这是为了做锁的重入操作. state的值代表当前这个锁被重入了多少次, exclusiveOwnerThread属性就是当前持有锁的线程的java thread对象.
-
如果先前得到的state状态不是0, 并且持有锁的对象又不是当前线程, 则返回false.
经过上面的分析可知, tryAcquire只会尝试一次cas, 一旦cas失败, 最多就判断一个重入, 然后就直接返回了. 就跟我在开篇写的tryAcquire一样, 开篇我的代码没有判断重入, 直接cas, 失败就返回false. 成功也就只是设置一下独占线程而已.
addWaiter方法
在tryAcquire方法中, 线程仅仅就是尝试了一次cas, 如果成功就加锁成功了, 如果失败, 则会在短路与中引发后面的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法的执行.
我们先看看addWaiter方法做了什么.
在调用addWaiter的时候, 传参是一个Node.EXCLUSIVE, 这个参数在形参中叫做mode, 实际上可以理解为一个加锁的模式, 因为AQS支持共享锁和独占锁两种模式, 在AQS中, 每个线程都被包装在一个Node对象中, 使用mode来表明当前线程加锁的方式是独占还是共享.
类名: java.util.concurrent.locks.AbstractQueuedSynchronizer.Node
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
可以看到, EXCLUSIVE就是一个null, 而SHARED是一个new出来的普通Node, 只不过是一个单例的.
在new Node的时候, 将nextWaiter属性设置为mode, 用以区分是独占还是共享. 并且在该构造函数中, 还传递了一个线程进去, 表明传进来的这个线程被封装在了一个Node对象中.
类名: java.util.concurrent.locks.AbstractQueuedSynchronizer.Node
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
具体addWaiter方法代码逻辑如下:
// 将当前线程包装成一个Node节点进入AQS的同步队列中, 并返回当前线程包装的Node
// mode是模式, 为null就是独占模式
private Node addWaiter(Node mode) {
// 将当前线程包装在一个new出来的Node对象中, 模式为mode
Node node = new Node(Thread.currentThread(), mode);
// 拿到同步等待队列的尾部节点tail
Node pred = tail;
// 判断tail是否为空, 如果不为空, 就说明同步队列中已经有元素了, 直接将当前线程包装的node插入尾部节点即可
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果当前队列为空, 就需要构造出同步等待队列, 但是这里却封装了一个方法, 原因是有并发问题
enq(node);
// 入队成功后, 返回了当前线程所包装的Node
return node;
}
进入addWaiter方法的前提是, tryAcquire失败了, 如果一个线程去争抢锁失败了怎么办? 在MESA模型中已经有过讲解, 此时的线程应该进入同步等待队列去等待. 所以addWaiter方法就是用来入队的.
AQS提供了2个属性来构造这个队列, head和tail, 分别是头结点和尾结点. 并且这个队列是一个双向链表结构.
AQS中的队列:
类名: java.util.concurrent.locks.AbstractQueuedSynchronizer
private transient volatile Node head;
private transient volatile Node tail;
Node构造双向链表:
类名: java.util.concurrent.locks.AbstractQueuedSynchronizer.Node
volatile Node prev;
volatile Node next;
在addWaiter方法中的注释也写的很清楚, 同时进入addWaiter方法的线程会有很多个, 因此当tail不为空的时候, 想要每个线程都安全入队, 必须使用cas去修改尾结点.
如果发现当前队列为空, 或者cas失败, 就将会进入enq方法中.
// 如果同步等待队列为空, 进入enq方法构造队列
// 之所以构造队列是因为可能会有多个线程一起使用同一个aqs对象, 同时发现等待队列为空, 同时构造队列的可能
// 多个线程同时入队, 最终构造的同步队列大概长这样: dummyNode -> thread1Node -> thread2Node -> ... -> threadnNode
// 并且n次, 每个线程将自己设置为tail节点, 每个线程执行成功后返回之前的tail节点
// 理论上来说, 除了第一次构造队列的情况, 其它情况当前持有锁的线程, 就是dummyNode(head)的线程
private Node enq(final Node node) {
// 自旋处理多线程入队
for (;;) {
Node t = tail;
// 先判断尾部节点是否为空, 为空就代表需要构造这个同步等待队列
if (t == null) { // Must initialize
// 这里使用了cas, 因为可能有多个线程一起走到这里, 只能有一个线程成功
// 将头结点设置一个无属性的最普通的dummyNode对象
if (compareAndSetHead(new Node())) {
// 成功的线程就将tail设置为head(因为这时候只有一个dummyNode节点), 继续自旋走到下面的else里
tail = head;
}
// 失败的线程将自旋回到上面, 当失败的线程回到上面之后, tail已经设置过了, 将会走到下面的else里
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
// 最终所有线程一定都会设置成功尾部节点, 每个线程返回每个线程的前面那个节点
t.next = node;
return t;
}
}
}
}
在构造队列的时候, 使用了一个很普通的dummyNode去作为头结点, 其它节点通过cas控制并发依次安全进入队列, 最终成功构造成这样的一个队列: dummyNode -> thread1Node -> thread2Node -> ... -> threadnNode
最终, addWaiter方法构造了一个队列, 并且将所有没有抢到锁的线程都放进了AQS的同步等待队列中, 并且每个线程在addWaiter方法结束时, 都分别返回了各自所属的Node对象.
acquireQueued方法
// 如果是ReentrantLock, arg就是1
// 这里传过来的Node就是当前的线程tryAcquire失败后所包装的Node
// 返回值: 如果在等待时被打断, 就返回true, 否则返回false
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 死循环自旋
for (;;) {
// 拿到当前node的前一个节点
final Node p = node.predecessor();
// tryAcquire的原因:
// 1. 这是一个循环, 在后面线程被park, 之后唤醒后依然从park走出来, 如果被唤醒就需要再次tryAcquire, 并且这里tryAcquire还能碰巧满足下面2和3的可能性
// 2. 可能前面持有锁的线程, 在当前这个线程走到这一步的时候已经放开锁了, 所以tryAcquire一次
// 3. 非公平状态下也可能会有其它线程过来加锁
if (p == head && tryAcquire(arg)) {
// 这里不使用cas是因为上面的tryAcquire只有一个线程可以成功
// 成功的线程将自己设置为头结点, 然后返回出去, 这样就代表加锁成功了
// 因此前面说的"理论上来说, 除了第一次构造队列的情况, 其它情况当前持有锁的线程, 就是dummyNode的线程"就是这个原因
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果当前node的前一个节点不是head
// 或者当前node的前一个节点是head, 尝试tryAcquire却失败了
// 就需要根据前一个节点的状态, 去判断当前线程是否需要被park
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
// 如果当前线程的打断标志是true, parkAndCheckInterrupt将会返回true, 此方法也会返回true了
interrupted = true;
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
acquireQueued方法进来就是一个死循环, 即使park之后, 再次唤醒, 依然会跳进循环的入口处.
node节点在new出来一直到入队后, 它的waitStatus一直都是0, 在shouldParkAfterFailedAcquire中会修改一次node的waitStatus, 如果waitStatus<=0并且waitStatus不为-1, 则cas将其修改为-1, 因为AQS设定为, 只有前面一个节点的状态为-1的时候, 其下一个节点才能被唤醒.
修改完waitStatus之后, if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())判断将会直接返回false, 则进入for循环的入口, 再次尝试tryAcquire, 失败后这时候当前node的前一个节点的waitStatus已经为-1, 那么就直接park当前没获取到锁的线程了.
当线程被唤醒之后, 继续从当前park的地方开始执行, 回到for循环入口, 开始tryAcquire, 成功则获取到锁, 失败则继续重复上述逻辑.
lock方法总结
-
tryAcquire, 判断当前state是否为0
1.1. state是0, 则cas修改state=s1. 成功则设置独占线程, 并且返回true, 失败返回false.
1.2. state不是0, 判断当前线程是否为锁的当前独占线程
1.2.1 当前线程是锁对象当前的独占线程, 则state=state+1, 返回true. 1.2.2 当前线程不是锁对象当前的独占线程, 则返回false. -
如果tryAcquire返回true, 那么lock方法将直接返回, 也就是放行当前线程, 获取锁成功.
-
如果tryAcquire失败, 则先调用addWaiter方法, 每个线程都被包装在一个Node里, nextWaiter属性设置为独占(null), 然后使用cas+for循环构造队列+线程节点入队, 每个线程调用的addWaiter方法都将返回各自自己的包装所在的Node节点对象.
-
线程Node入队之后, 调用acquireQueued方法, 传入当前线程包装Node对象. 在for循环中park, 一旦被唤醒, 将再次从park位置运行, 回到for循环入口, tryAcquire, 如此往复.
unlock方法
当调用unlock方法时, 需要明确调用线程的状态, 如果一个线程能够走到调用unlock方法这一步, 说明它一定是获取锁的线程, unlock的逻辑是先修改state状态, 再去唤醒处于同步等待队列的线程, 在修改state状态的时候, 其它线程还处于park阶段, 因此不会有其它线程去进行tryAcquire修改state, 所以可以直接修改state状态. 在ReentrantLock中, 直接调用release方法释放锁.
类名: java.util.concurrent.locks.ReentrantLock.Sync
public void unlock() {
sync.release(1);
}
类名: java.util.concurrent.locks.ReentrantLock.Sync
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release方法很简单, 从代码结构上看, 大概就是先释放锁, 如果同步等待队列不为空, 则唤醒继承者(head的下一个节点), 然后直接返回true.
tryRelease方法
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;
}
由于tryRelease的时候, 其它线程都还在park中, 因此直接修改状态state=state-1, 并且判断当前状态修改后是不是0, 如果不是0, 说明是重入, 还需要等下一次, 所以free=false, 如果state-1后等于0, 说明已经是最后一次调用unlock了, 那么就把当前锁对象的独占线程设置为null, 并且返回true.
因此, tryRelease方法返回为true, 就代表锁释放了, 为false就代表是一个重入情况, 并没有完全释放.
release方法
当tryRelease方法返回false时, 也就是说是重入情况时, release方法就会直接返回false, 等待下一次unlock.
当tryRelease方法返回true时, 就说明当前state已经空闲了, state=0了, 那么就判断等待队列中是否有线程node在等待, 如果有, 就唤醒.
方法中是通过head判断的, 并且唤醒的也是head节点的下一个节点(head节点相当于一个dummy节点).
如果当前head节点不为空, 并且head节点的waitStatus不是0, 那么就执行unparkSuccessor方法, 传入head.
unparkSuccessor方法
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);
}
先cas修改head节点的waitStatus状态为0(我不知道为什么要cas, 想象不到有什么场景能使head节点的waitStatus状态改变).
再拿到head节点的下一个节点, 直接unpark线程, 这样, 之前在同步等待队列中park的线程, 就能回到for循环的入口处, 进行tryAcquire了.
我们再来重复看看那个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(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
如果唤醒节点的上一个节点是head, 并且它tryAcquire改state成功了, 就设置头结点为当前被唤醒的node, 然后将先前的head节点与当前的node节点断开连接, 让先前的那个head节点等待GC回收.
条件等待队列
前面的篇幅主要介绍锁的加锁和解锁的过程, 但是对于MESA来说还远远不止这些, 线程间的wait/notify机制也是需要实现的, 并且MESA模型也规定必须存在一个/多个条件等待队列.
条件等待队列主要用于存放那些经过判断发现条件不满足的线程, 这些线程拿到锁之后, 做了判断, 认为自己不应该继续执行下去, 于是就阻塞自己, 并且释放锁, 让其它线程拿到锁, 等其它线程判断条件满足之后, 再通知之前条件不满足的线程继续运行. 那么那些条件不满足的线程在阻塞的阶段, 就需要有一个队列去存放它们, 这个队列就是条件等待队列.
先看API:
public class ConditionTest {
static boolean hasCigarette = false;
static boolean hasEncourager = false;
public static void main(String[] args) throws Exception {
Lock lock = new ReentrantLock();
Condition cigarette = lock.newCondition();
Condition encourager = lock.newCondition();
new Thread(() -> {
lock.lock();
while (!hasCigarette) {
try {
System.out.println(String.format("[%s]没有烟, 罢工", Thread.currentThread().getName()));
cigarette.await();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
System.out.println(String.format("[%s]有烟了, 开搞", Thread.currentThread().getName()));
}, "needCigarette").start();
new Thread(() -> {
lock.lock();
while (!hasEncourager) {
try {
System.out.println(String.format("[%s]没有鼓励师, 罢工", Thread.currentThread().getName()));
encourager.await();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
System.out.println(String.format("[%s]有鼓励师了, 开搞", Thread.currentThread().getName()));
}, "needEncourager").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
lock.lock();
try {
hasCigarette = true;
System.out.println(String.format("[%s]送上烟", Thread.currentThread().getName()));
cigarette.signalAll();
} finally {
lock.unlock();
}
}, "eleme").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
lock.lock();
try {
hasEncourager = true;
System.out.println(String.format("[%s]送上鼓励师", Thread.currentThread().getName()));
encourager.signalAll();
} finally {
lock.unlock();
}
}, "meituan").start();
}
}
运行结果:
[needCigarette]没有烟, 罢工
[needEncourager]没有鼓励师, 罢工
[eleme]送上烟
[needCigarette]有烟了, 开搞
[meituan]送上鼓励师
[needEncourager]有鼓励师了, 开搞
代码中needCigarette线程运行后, 发现没有烟, 于是调用await方法, 停止运行, 并且释放了锁, 因此needEncourager线程才能抢到锁, 但是needEncourager线程抢到锁之后, 发现没有鼓励师, 也await了, 于是eleme线程就能抢到锁了, eleme修改了hasCigarette为true, 并且使用signalAll方法唤醒了所有在条件等待队列中的线程, 于是两个程序员线程又开始竞争锁, 但是其中需要烟的程序员发现有烟了, 于是继续运行, 释放锁之后, 需要鼓励师的程序员抢到了锁, 但是发现条件还是不满足, 于是又await了, 再等待meituan线程带来了鼓励师, needEncourager线程才能继续运行下去.
其中newCondition方法, 就是创建一个条件对象, 在juc中的实现是一个ConditionObject类, 一个这样的类的对象就对应一个条件队列.
类名: java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
// 省略亿行代码...
}
其中firstWaiter就是条件队列的头结点, lastWaiter就是条件队列的尾节点, 节点的类型依然是Node, 节点之间通过nextWaiter属性串联起来. 需要注意的是: 条件等待队列只是一个单向链表, 而同步等待队列是一个双向链表.
newCondition方法
newCondition方法非常简单, 调用了Sync的newCondition方法, 直接new出一个ConditionObject.
类名: java.util.concurrent.locks.ReentrantLock.Sync
final ConditionObject newCondition() {
return new ConditionObject();
}
而ConditionObject的构造函数也非常简单, 根本没有属性填充.
public ConditionObject() { }
await方法
在await方法的上面, 作者对方法的过程做了解释, 但是依然还是比较晦涩的
- If current thread is interrupted, throw InterruptedException.
- Save lock state returned by getState.
- Invoke release with saved state as argument, throwing IllegalMonitorStateException if it fails.
- Block until signalled or interrupted.
- Reacquire by invoking specialized version of acquire with saved state as argument.
- If interrupted while blocked in step 4, throw InterruptedException.
await方法比较复杂, 涉及到许多种情况, 比较绕. 代码如下:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter方法
当一个线程调用await方法的时候, 说明它的条件已经不满足了, 但是在开始执行await方法的那一刻, 它还是占有锁的, 并且其它线程还处于park中, 所以对于条件等待队列来说, 这时候是没有线程安全问题的. 所以addConditionWaiter方法直接使用普通的入队即可, 首先判断尾节点是否为空, 为空就构造条件等待队列, 不为空就将当前线程包装的Node赋值给尾结点的nextWaiter属性.
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
new Node的时候, waitStatus指定位Condition, 也就是-2.
fullyRelease方法
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
fullyRelease方法先获取了加锁的状态, 通过getState, 然后将state传入release方法, 进行释放锁.
之前我们看到对于加锁或者释放锁来说, 只要传入1就可以了, 这里之所以传入的不是1, 是因为可能当前线程是重入过这个锁的, 在多次重入后的某个方法中判断到条件不满足, 那么就需要fully释放.
由于截止目前, 当前需要await的线程依然还是独占锁, 所以不存在线程安全问题, 直接普通修改state即可.
release方法释放锁之后, 还会继续唤醒处于同步等待队列中的下一个节点. 之前有看过, 所以就不继续看了
isOnSyncQueue循环
await的线程释放锁之后, 唤醒了同步等待队列的线程, 这时候之前处于同步等待队列的线程就可以进行tryAcquire了, 当然, 如果这是一个非公平锁, 可能还会有其它外部新来的线程进行lock, 但是这已经和当前await的线程没有关系了, 当前await的线程还需要将自己阻塞, 等待被其它线程signal.
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
当一个线程首次自己调用await方法的时候, 由于构造了一个新的Node, 于是这个Node对象肯定不是在同步等待队列中的, 自然就进入了循环体中, 被park了, 于是await方法到这里就结束了.
这个循环的意义在于, 当这个被park的线程被signal唤醒, 首先这个node对象将会被移动到同步等待队列中, 然后继续从循环体中的park的位置继续执行, 回到while的判断条件中, 这时候它已经是在同步等待队列中了, 于是就不会进入这个while循环中. 这点将会在后面分析signal中讲到.
signalAll方法
与synchronized提供的notify方法一样, 不允许在当前调用signalAll方法的线程未获取该对象的锁的情况下调用, 否则将抛出IllegalMonitorStateException.
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
因此, 在某个线程开始调用signanlAll方法的时候, 这个线程一定是独占这把锁的, 所以当前依然还是不会有线程安全问题, 直接可以使用普通的方法将条件等待队列置空, 然后依次将这个条件等待队列中的线程包装Node节点移动到同步等待队列中, 实现在doSignalAll方法中.
doSignalAll方法
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
在调用doSignalAll方法的时候, 在方法外部传入的时候, 传入的是firstWaiter的指针, 于是在方法内部, 直接将firstWaiter和lastWaiter置空, 然后通过传进来的first指针依次调用transferForSignal方法进入同步等待队列中.
final boolean transferForSignal(Node node) {
// If cannot change waitStatus, the node has been cancelled.
// cas失败的情况下是node被取消了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
enq方法很熟悉, 将前面传进来的原本处于条件等待队列的node对象, 加入到同步等待队列中, 并且在前面已经将节点的waitStatus通过cas设置为了0.
通过获取到的waitStatus, cas修改当前node节点的waitStatus状态为SIGNAL, 如果成功了, 就不管了, 因为按照正常的书写逻辑, 将会在调用signalAll的线程中在后续的unlock方法中唤醒同步等待队列中的线程.
至于这里的cas失败的情况, 将会唤醒这个cas失败的线程, 于是这个线程就会在await方法它park的地方重新开始运行, 然后再会阻塞于acquireQueued方法中, 又进入了同步等待队列.
于是signalAll方法就结束了.
signal方法
signal调用doSignal方法, doSignal方法非常简单, 直接唤醒条件等待队列的头结点, 只要移动到同步等待队列成功, 那么这个do...while...循环将只会做一次循环. 如果失败, 就是线程被取消了, 那么就顺延往后signal条件等待队列的其它节点.
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
回头再看await方法
上面说到, 当把条件等待队列中的node移动到同步等待队列之后, signal/signalAll方法就结束了.
按照正常的书写逻辑, 将会在finally块中使用unlock方法, 使调用signal/signalAll方法的线程释放锁, 释放锁的时候, 前面讲过, 将会使用unpark唤醒head节点的下一个节点.
对于调用过await方法的线程来说, 它之前park的地方处于await方法的isOnSyncQueue判断的那个while循环中的park位置. 它先是在signal中被移动到了同步等待队列, 后续又被unlock中的unpark唤醒, 那么它复苏的地方自然也就是isOnSyncQueue方法的park位置.
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
// 如果不是被打断复苏, 将会重新来到while判断处, 由于被signal的线程处于同步等待队列中, 因此不会再进入while循环
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 复苏之后, 先判断本次复苏是不是由于被打断而复苏
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
复苏之后, 先是判断本次复苏是为什么复苏的, 因为线程复苏也可能是被interrupt打断了, 这里首先判断是不是由于被打断而复苏, 如果不是, 将会回到循环体条件判断入口处, 此时它肯定是已经处于同步等待队列了, 所以while循环将不会进去. 接下来就是acquireQueued了, 获取锁, 如果没有获取成功, 则继续在同步等待队列待着.
await方法的InterruptedException
后续的几个判断都是基于其它一些异常情况, 比如线程被取消, 线程被打断.
对于await方法, 如果线程是被打断而复苏, 则应该抛出IE异常, 但是抛出异常, 首先它得获取锁吧? 如果不获取到锁, 怎么去执行抛出异常的方法呢? 所以判断复苏是被打断而复苏, 就将被打断标记设置在interruptMode这个变量中, 后续直接在本方法里抛出IE异常了, 并且也不应该继续判断是否在同步等待队列, 因为它肯定不在, 所以while循环被break跳出.
接下来被打断的线程就会去acquireQueued, 去获取锁, 如果被打断的await线程没有获取锁成功, 则会被park在await方法中的acquireQueued位置(代码处于这里, 但是节点处于同步等待队列), 直到下次被唤醒, 继续从这里执行开始执行, acquireQueued正常情况下肯定会返回true(正常情况下不是true就是阻塞), 然后根据interruptMode判段是否应该在本await方法中抛出IE, 实际上是应该的, 所以就在reportInterruptAfterWait中抛出了IE.
总结
本文通过对源码的逐行分析, 通过ReentrantLock的加锁解锁逻辑, 讲述了AQS的代码逻辑.
AQS作为抽象队列同步器, 自带了acquire和release方法, 而对于tryAcquire和tryRelease方法, 则是交给派生类去实现.
ReentrantLock基于MESA模型, 使用java代码写了一套加锁解锁, 等待唤醒的机制. 与synchronized不同的是, synchronized只能支持一个条件等待队列, 而ReentrantLock可以自定义多个条件等待队列.
公平/非公平
ReentrantLock支持公平和非公平, 实际上公平和非公平区别非常小, 仅仅是 tryAcquire方法不太相同, 对于非公平来说, 新来的线程会在tryAcquire方法中判断state为0时直接进行cas修改state, 而对于公平锁来说, 如果新来的线程发现state是0, 也会先判断同步等待队列中是否有元素, 如果有元素, 则不会进行cas改状态, tryAcquire方法直接返回false, 进而在acquireQueued中进入同步等待队列.