AQS是j.u.c包下的一个框架,被用来实现各种锁以及其他的同步框架,比如ReentrantLock,Semophare,CyclicBarrier,CountDownLatch等。
AQS的数据结构(无序列表乱码了,编辑器内正常)
-
CLH队列
-
AQS中是通过一个CLH队列来实现线程的阻塞,唤醒的。
-
该队列是一个双向链表,每个节点代表了一个线程,每个节点都有prev和next两个指针。
-
节点还有一个waitStatus的int类型的状态,用来表示该线程当前的状态。包含
-
cancelled
-
signal
-
condition
-
propagate
-
新来的线程如果acquire失败,会包装成一个新的Node放到链表尾部(尾插法)
-
之前的线程如果release成功,会唤醒下一个线程,也即head的下一个节点。head本身是一个虚节点。
-
state
-
状态属性,int类型,volitile修饰。用来表示资源
-
举例
-
ReentrantLock
-
0 代表锁没有被任何线程持有
-
大于0 代表锁被某线程持有,具体数值代表持有次数
-
CountDownLatch
-
countDown方法会将state减一
-
await会阻塞直到state减到0
ReentrantLock
下边我们结合ReentrantLock来具体分析下AQS的相关源码
lock的执行过程
1.lock方法实际调用的是acquire方法,acquire代码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先tryAcquire一次,如果成功了,代表获取锁成功;如果失败,则包装成一个Node尾插到CLH队列尾部,同时对该Node进行acquireQueued操作(下边会介绍acquireQueued方法)。
2.tryAcquire方法是我们需要实现的逻辑:
以非公平锁为例:
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;
}
首先我们判断当前锁是否被持有,如果没有,通过CAS的方式设置成持有状态,并将本线程设置为持有线程,返回true,代表lock成功;如果当前锁已经被持有且是被当前线程持有,则增加state,代表重入+1,返回true,代表lock成功;否则代表当前锁被其他线程持有,返回false,代表lock失败。
3.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);
}
}
for循环内,如果node的前驱节点是头节点,即node是第一个非虚节点(head是虚节点),当前线程(也即node的线程)才有资格去继续执行tryAcquire的逻辑。如果acquire成功,会将该节点设置为head,并将该节点的thread,prev设置为null,使其也成为一个虚节点,保持head虚节点的特性。
如果node的前驱节点不是head,满足条件时会将当前线程(也即node的线程挂起)。
到此,整个lock(acquire)的过程结束。
简单来讲,首先尝试一次,如果成功返回;如果失败会放入队列尾部,继续尝试,但其实只有当前node成为第一个非虚节点才真正有资格进行tryAcquire的操作。这个地方会挂起避免空转。当持有线程释放时,会唤醒后边的节点(下边释放逻辑会讲到)。
unlock的执行逻辑
1.release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
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;
}
由于ReentrantLock是可重入的,因此tryRelease如果只是减少了重入次数,不代表真正释放资源,会返回false;只有完全释放资源,也即state变为0,才会返回true。
release方法中,当tryRelease返回true,即当前线程(也即调用unlock的线程,也即获取到锁资源的线程)真正释放资源时,才会去唤醒head的后继节点的线程(unparkSuccessor(head))。而被唤醒的线程会去执行前边分析的acquireQueued逻辑,尝试获取资源(如果是第一个非虚节点的话)。
由此及acquireQueued中的tryAcquire方法可知,CLH队列管理的线程获取资源是按照先来后到的,是公平的。