1、AbstactQueuedSynchronizer的基本数据结构
1.1 AbstractQueuedSynchronizer的等待队列是CLH队列的变种,CLH队列通常用于自旋锁,AQS中的CLH可以简单的理解为“等待锁的线程队列”。
1.3 一条线程所在节点如果它处于队列头的下一个节点,那么它会尝试去acquire。因为头节点是一个dummy节点,也就是说头节点不持有任何线程。所以,一条线程所在节点如果它处于队列头节点的下一个节点,那么他会尝试去acquire,但是并不保证成功。
1.4 要进入队列,你只需要自动将它拼接在队列尾部即可;如果获取了锁,你只需要设置header字段即可。
2、在“不响应中断的独占锁”模式下AbstractQueuedSynchronizer供子类实现的方法
3、在“不响应中断的独占锁”模式下AbstractQueuedSynchronizer获取锁的实现流程
1. addWaiter(Node.EXCLUSIVE), arg),将当前线程封装成一个节点,添加到“等待锁的线程队列”中去。
2. acquireQueued,当前线程所在节点目前在“等待锁的线程队列”中,当前线程仍然尝试从等待队列中去获取锁。
如果尾节点为空,即当前数据结构中没有节点,那么new一个不带任何状态的Node作为头节点,并且将head赋值给tail。如果尾节点不为空,那么并发下使用CAS算法将当前Node追加成为尾节点,由于是一个for(;;)循环,因此所有没有成功acquire的Node最终都会被追加到数据结构中。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这里会判断当前节点的前驱节点的状态,如果:
(1). 如果 当前节点的前驱节点的 waitStatus是SIGNAL,返回true,表示当前节点应当park。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1. 调用自定义同步器的tryAcquire尝试直接去获取资源,如果成功则直接返回;
2. 没成功,则addWaiter将该线程加入等待队列的尾部,并标记为独占模式;
3. acquireQueued使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。由于此函数是重中之重,我再用流程图总结一下:
4、在“不响应中断的独占锁”模式下AbstractQueuedSynchronizer释放锁流程
上一小节已经把acquire()说完了,这一小节就来讲讲它的反操作release()吧。此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。下面是release()的源码:
逻辑并不复杂。它调用tryRelease来释放资源。有一点需要注意的是,它是根据tryRelease的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease的时候要明确这一点!!跟tryAcquire一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release是根据tryRelease的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。如果没有彻底释放资源,也就是出现了重入的情况,需要多次释放。如果释放成功了,我们就需要唤醒head节点的下一个节点所持有的线程。基本逻辑如下:
1. 首先拿到head节点,判断head节点不等于null,并且head节点的waitStatus是不等于0的话,就去唤醒head节点的下一个节点所持有的线程。
2. 调用unparkSuccessor(Node node)方法唤醒head节点的下一个节点所持有的线程。
接下来看下unparkSuccessor(Node node)方法的源代码:
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);
}
这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃的线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃的线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!
参考文献:
1. 再谈AbstractQueuedSynchronizer1:独占模式
2. Java并发之AQS详解