重读 AbstractQueuedSynchronizer
讲道理,JUC 不只是代码写得好,注释写的也是真的好啊
1. AQS API
AQS 的定位在于实现了一个同步器的框架,能够作为各种同步器的基础。整体来说,这个同步器框架为你提供了一套基础的 API,包括但不限于:
acqure- 独占式的抢占锁acquireInterruptibly- 独占式抢占且响应中断异常tryAcquireNanos- 尝试在特定时间内抢占acquireShared- 共享式抢占(与独占互斥)release- 释放抢占的独占锁releaseShared- 释放抢占的共享锁
这些API 可以用来实现各种锁,但在 AQS 的实现来说,它的实现描述了如何控制:
- 多个线程如何争夺队首来获得同步状态
- 争夺失败的线程如何排队、阻塞和被唤醒
并且,由于它采用了模板设计模式,任何人可以轻易的扩展 AQS 来实现自定义的同步器和锁。
在看同步接口的实现之前,先看同步队列的结构,以及节点如何入队。
2. CLH lock 队列
AQS 内部维护着一个 FIFO 队列,被称为CLH (Craig,Landin,Hagersten)lock 队列。CLH 队列通常用于提供自旋功能,但在 AQS 中被用于实现一个阻塞的同步器。
+-------+ prev +------+ +------+
head | | <----> | Node | <----> | | tail
+-------+ next +------+ +------+
队列中的每个 Node 代表一个线程,线程抢夺失败之后通常就会被放进队里排队等待。
2.1 Node
数据结构 Node 是 CLH queue 中的单元类型:
- 队列里每个
Node实例都作为一个特定线程的代表,持有一个等待的线程。 - 每个 Node 都包含了一个
waitStatus字段,这个字段用于跟踪该节点所代表的线程是否应该被阻塞。 - 当前
Node的前驱节点被释放时,当前节点会收到通知。 - 每个
Node可能会多次尝试获取锁,但并不保证成功,如果失败了,就可能需要重新等待。
Node 结构
thread: 节点对应的线程prev,next: 前驱节点和后置节点waitStatus: 标记当前节点对应的线程状态SIGNAL:-1, 表示该节点的后继已经被阻塞或者即将被阻塞。因此,当前节点在解锁释放时,需要对后继节点执行 unpark。CONDITION:-2, 表示当前节点在 condition queue 中。PROPAGATE:-3, 下一次共享式同步状态获取将会无条件地传播下去CANCELLED:1, 表示当前节点已经被取消。0:不是上面的任何一种,是一种默认状态,会在处理的过程中被设置为上面的一种- 从值得分布来看,当
waitStatus值为非负数,则肯定不需要被唤醒,因此在实际使用的很多时候不需要判断具体的值是多少。 - 所有的修改都通过 CAS 进行。
nextWaiter:TODO
// 构造方法
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
2.2 入队
... 更多,见重读 AbstractQueuedSynchronizer ...