JUC | AQS 源码
AQS-CLH
AQS 是一个抽象的类,以在 ReentrantLock 中的实现为例学习一下 AQS 的相关操作。
AQS 中维护了一个 volatile int state 的变量,这个 state 代表的是临界资源,还有 FIFO 的线程等待队列。
state 在不同实现中代表的意思可能不同,在 ReentrantLock 中 state=1 表示已经被锁定,此时其他线程再尝试加锁会失败,失败的线程会被放入线程队列中,同时 ReentrantLock 是可重入锁,表示获得一把锁的线程可以再次获取到这把锁,只需要 state + 1 即可,同样解锁的时候需要 state -1 。
AQS 的结构:
第一个线程 - 获取锁 -> 成功
第一个线程可以成功的获取到锁。然后设置这个线程为独占线程
此时,如果再有第二个线程来获取锁时,由于是非公平锁,所以上来先尝试争抢锁,线程1还没有释放锁,因此争抢失败。
public final void acquire(int arg){
if(!tryAcquire(arg)&&//线程2 尝试争抢锁失败
// addWaiter 返回一个包含当前线程的 node 节点,实际也是链表的尾部节点。
acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
如果当前节点的前置节点是 head 就尝试争抢锁,如果抢到就把当前节点设置为 head 节点,如果前置节点不是 head 或者 争抢锁失败,就会通过 shouldParkAfterFailedAcquire 方法将前置节点的 waitStatus 设置为 -1,再通过 parkAndCheckInterrupt 方法将当前的线程挂起。
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);
}
}
在 ReentrantLock 中,如果争抢锁的线程仍然是获取锁的这个线程,那么是可以成功获取到锁的,只是 stata 的值会 + 1。
第二个线程 - 获取锁 -> 失败
线程二执行 tryAcquire()
后会返回 false,接着执行 addWaiter(Node.EXCLUSIVE)
逻辑,将自己加入到一个 FIFO
等待队列中。addWaiter()
方法执行完后,会返回当前线程创建的节点信息。继续往后执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
逻辑。
第三个线程 - 获取锁 -> 失败
线程三执行 tryAcquire() 也会返回 false,接着执行 addWaiter() 逻辑,此时等待队列的tail
节点指向线程二,进入if
逻辑后,通过CAS
指令将tail
节点重新指向线程三。接着线程三调用enq()
方法执行入队操作,和上面线程二执行方式是一致的。
第一个线程 - 释放锁
首先是 线程一 释放锁,释放锁后会唤醒 head
节点的后置节点,也就是我们现在的线程二,具体操作流程如下:
释放锁代码:
public final boolean release(int arg){
if(tryRelease(arg)){
Node h=head;
if(h!=null&&h.waitStatus!=0)
unparkSuccessor(h);
return true;
}
return false;
}
这里首先会执行tryRelease()
方法,这个方法具体实现在ReentrantLock
中,如果tryRelease
执行成功,则继续判断head
节点的waitStatus
是否为0,前面我们已经看到过,head
的waitStatue
为SIGNAL(-1)
,这里就会执行 unparkSuccessor()
方法来唤醒 head
的后置节点,也就是我们上面图中线程二对应的Node
节点。