针对性重复练习。持续做你不会做的事。
Node类
Node节点组成的队列是CLH(Craig, Landin, and Hagersten)的变种。CLH通常作为自旋锁使用。我们将它作为阻塞同步器使用,但是前驱节点持有线程的控制信息的策略是相同的。每个节点中的状态字段保存线程的阻塞信息。前驱节点释放时,当前节点将得到通知信号。每个队列中的节点作为specific-notification-style监视器,持有唯一的等待线程。队列中的第一个线程有机会尝试获取锁,但是不保证一定成功获得(非公平锁),它只是给了线程竞争的权利,所以被释放的竞争者可能再次等待。1
2
3 +------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
进入到CLH队列,你只需要一个原子操作将它拼接在队列尾部。出队,你只需要重置头部节点。
插入CLH队列需要一个放入尾部的原子操作,因此存在一个指针指向队中和非队中的分界点。出队需要更新“head”指针。CLH队列需要花费大量的工作决定队首能否成功获取锁,还需要花费一部分时间处理超时和中断导致的任务取消。
pre指针链接的链表主要保存取消节点。如果一个节点已经取消,它的后继指针被连接到
一个未取消的前驱节点。
我们使用next指针的链接的链表实现阻塞队列机制。每个节点内部保存线程的id,所以一个前驱节点通过遍历next指针查找下一个应该被唤醒的节点。确认后继者过程必须避免新入队节点设置next指针的并发竞争。
Node源码
1 |
|
addWaiter入队操作
利用当前线程和给定的模式创建Node节点并且入队。head和tail字段的初始化在这个操作中完成。初始化过程:
- 如果tail字段为null,生成一个新的Node节点。
- head和tail指向这个新节点。
- 新的节点waitStatus为0。
1 |
|
1 | private Node enq(final Node node) { |
acquire独占模式获取锁操作
子类通过调用这个方法获取锁。取得锁的规则有tryAcquire方法持有,子类重写这个方法而且不会阻塞线程。
1 |
|
tryAcquire失败线程会加入队列 线程可能会反复的被阻塞和唤醒直到tryAcquire成功,这是因为线程可能被中断, 而acquireQueued方法中会保证忽视中断,只有tryAcquire成功了才返回。
中断版本的独占获取是acquireInterruptibly方法,doAcquireInterruptibly这个方法中如果线程被中断则acquireInterruptibly会抛出InterruptedException异常。addWaiter方法只是入队操作,acquireQueued方法是主要逻辑,需要重点理解。
1 |
|
shouldParkAfterFailedAcquire方法的作用是:
- 确定后继是否需要park;
- 跳过被取消的结点;
- 设置前继的waitStatus为SIGNAL.
1 |
|
线程被唤醒只可能是:被unpark,被中断或伪唤醒。被中断会设置interrupted,acquire方法返回前会 selfInterrupt重置下线程的中断状态,如果是伪唤醒的话会for循环re-check。
1 | private final boolean parkAndCheckInterrupt() { |
独占模式释放
比较简单只要直接唤醒后续结点就可以了,后续结点会从parkAndCheckInterrupt方法中返回。
1 |
|
1 |
|
acquireShared共享获取模式
获取共享锁过程与独占模式基本相同。
- 尝试获取加入队列失败加入队列
- 如果是队首再次获取锁
- 再次失败挂起当前线程,等待被唤醒。
与独占模式不同的是成功获取共享锁之后调用setHeadAndPropagate,继续向后遍历队列,寻找相邻的共享节点给予锁。
1 | public final void acquireShared(int arg) { |
setHeadAndPropagate方法会将node设置为head。如果当前结点acquire到了之后发现还有许可可以被获取,则继续释放自己的后继, 后继会将这个操作传递下去。这就是PROPAGATE状态的含义。
1 | private void setHeadAndPropagate(Node node, int propagate) { |
参考文章:http://www.cnblogs.com/zhanjindong/p/java-concurrent-package-aqs-AbstractQueuedSynchronizer.html