队列同步器的实现分析
写在前边:队列同步器的实现分析包括同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放、超时获取同步状态等。受限于篇幅,本篇先分析了同步队列、独占式同步状态获取与释放。同时,阅读本篇前,推荐先阅读Java中的锁(1)。
1. 同步队列
●同步器内部维护了一个同步队列(FIFO双向队列)来管理线程的排队工作。当前线程获取同步状态失败时,同步器会把当前线程以及相关信息构造成一个节点(Node)加入到同步队列的队尾,并阻塞当前线程。当同步状态被释放后,同步队列中首节点将被唤醒,且将参加到获取新一轮同步状态的竞争中去。
● 需要注意的是,同步队列中的首节点是获取同步状态成功的节点。首节点在释放同步状态后,会唤醒其后续节点,后续节点在获取同步状态成功后将自己设置为首节点。
2. 独占式同步状态获取与释放
●调用同步器的acquire(int arg)方法可独占式获取同步状态,该方法如下:
public final void acquire(long arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
复制代码
-
1.tryAcquire(int arg)方法先尝试非阻塞获取同步状态,若成功,则该方法直接返回;否则执行第2、3步。
-
2.addWaiter(Node mode)表示因获取同步状态失败将当前线程及相关信息构造成一个节点,并 将其加入到同步队列的队尾。
-
3.acquireQueued(Node node,int arg)方法表示让该节点以自旋(死循环)的方式不断尝试获取同步状态。
●关于addWaiter(Node mode)方法的分析如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试在同步队列的队尾添加node
Node pred = tail; //同步器拥有首节点(head)和尾节点(tail)
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
复制代码
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
复制代码
- 1.compareAndSetTail(Node expect, Node update)保证node能被安全地添加到同步队列。若不这么做,可能在同一时刻有多个节点准备同时添加到同步队列的队尾,最终的结果是节点的数量有偏差且顺序也是混乱的。
- 2.在调用enq(final Node node)方法以前,若节点入队成功,则不必调用enq(final Node node)方法程序就返回了;若入队失败,调用enq(final Node node)通过"死循环"的方式保证节点一定能入队成功。
●关于acquireQueued(Node node,int arg)方法的分析如下:
final boolean acquireQueued(final Node node, long 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);
}
}
复制代码
- 1.acquireQueued(Node node,int arg)表示节点进入队列后以自旋的方式不断地尝试获取同步状态,获取成功则从自旋中退出。
- 2.只有前驱节点(prev)是首结点(head)的节点才能尝试获取同步状态,原因有三:
-
- ●维护同步队列的FIFO原则。
-
- ●同步队列中的首结点是获取同步状态成功的节点,而首节点在释放了同步状态后会唤醒后续的节点,后续节点被唤醒后需要检查自己的前驱节点是否为首节点。
-
- ●被过早通知(过早通知是指前驱节点不是首节点的节点由于中断而被唤醒)的节点若成功获取到同步状态,会和"同步队列首节点是成功获取同步状态的节点"这一原则相违背。
●同步器释放同步状态的方法如下:
public final boolean release(long arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //唤醒首节点的后续节点线程
return true;
}
return false;
}
复制代码