Java并发编程的艺术(七)——Java中的锁(2)

队列同步器的实现分析

写在前边:队列同步器的实现分析包括同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放、超时获取同步状态等。受限于篇幅,本篇先分析了同步队列、独占式同步状态获取与释放。同时,阅读本篇前,推荐先阅读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. 1.acquireQueued(Node node,int arg)表示节点进入队列后以自旋的方式不断地尝试获取同步状态,获取成功则从自旋中退出。
  2. 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;
   }
复制代码

总结:在获取独占式同步状态时,同步器维护一个同步队列,获取同步状态失败的节点将加入到队列里,并通过自旋不断尝试获取同步状态。移出队列的条件是前驱节点为首节点且成功获取了同步状态。释放同步状态将唤醒首节点的后续节点。

猜你喜欢

转载自juejin.im/post/5cfa1f3251882539c33e4f52