关于java:AbstractQueuedSynchronizer自顶向下分析三-acquire那些事儿

30次阅读

共计 3344 个字符,预计需要花费 9 分钟才能阅读完成。

概述

咱们采纳自顶向下的思路来逐渐深刻源码,首先剖析下 acquire 这个办法,顾名思义,很多 lock 办法的实现都是基于这个办法,他提供了一个获取许可的形象

acquire

该办法不响应中断,提供获取许可的性能,其中 tryAcquire 是个 protected 办法由子类实现,暂不剖析,整个办法用来实现 lock 办法
如果第一次获取锁失败则调用 acquireQueued 办法入队

 public final void acquire(int arg) {if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 为何要自我中断呢?selfInterrupt();}

addWaiter

问题 1 指定独占还是共享模式让以后线程入队,官网正文说是为了性能优化,优化的点在哪呢?貌似还是要判断一次非 null

 private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure 性能优化尝试 enq 办法的疾速执行门路,否则尝试残缺的 enq 办法
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // 这里都是对共享状态 tail 的操作,须要 CAS 保障
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

enq

节点入队办法,须要保障多线程并发插入时的正确性。
值得注意的是入队永远是在 tail 节点批改,也就是说只须要批改旧的 tail 的 next 指针,node 的 prev 指针以及 tail 指针

tail 节点为空,保障只有一个线程去初始化 head 节点和 tail 节点
tail 节点不为空,它的批改步骤如下
1.node.prev=tail。tail 是 volatile 的,所以后执行的线程肯定读到的是最新值,并把 node 的 prev 指向这个最新值
2. 原子更新新的 tail 节点为入参节点。为什么须要原子? 首先 tail 节点是共享状态,必须保障在正确设置了新 tail 节点的前提下设置旧 tail 节点的 next 指针,否则会呈现线程 A 批改了 tail 节点,还未修改旧 tail 节点,线程 B 染指批改了 tail 节点和旧的 tail 节点,导致线程 A 的新 tail 节点和线程 B 的旧 tail 节点更新失落
3. 更新旧的 tail 节点的 next 指针为入参节点,返回旧的 tail 节点


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;
                }
            }
        }
    }

acquireQueued

final boolean acquireQueued(final Node node, int 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);
        }
    }

剖析

 private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
        // 把节点绑定的线程赋 null,帮忙 gc 回收
        node.thread = null;

        Node pred = node.prev;
        while (pred.waitStatus > 0)
        //step1 node.prev 指针批改
            node.prev = pred = pred.prev;
        Node predNext = pred.next;
        // 只有这里才设置为已勾销状态
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

node.prev 指针批改剖析

咱们必须晓得的一点是 CLH 队列新增节点永远是 tail 节点。step1 看似没有任何的线程平安,实际上也无需 CAS 来保障。其实这里对共享状态的批改就一处
1.node.prev=pred,这里就是对共享节点的 prev 指针赋值,能够看到其依赖于局部变量 pred,所以它的线程安全性的剖析就变成了 pred 变量的剖析。而 pred=pred.prev,pred.prev 有可能被另一个线程批改,导致 pred 不统一吗?不可能!因为 prev 指针的批改只会在增加节点的时候,而增加节点又只会产生在 tail 节点,所以无论线程执行程序如何,最终都指向了同一个 pred,因而整个语句就是线程平安的。

node 为 tail 节点


1. 原子设置 tail 节点为找到的 pred 节点
为何须要原子?因为 tail 节点不牢靠,tail 节点在有新的线程增加节点的时候就会扭转,如果不必 CAS 保障就有可能笼罩了新增加的节点导致节点失落。
2. 原子设置 pred 节点的 next 指针为 null
这里的原子操作就看不太懂了,如果只是 pred.next=null 仿佛也没问题吧?毕竟第 1 步中曾经保障了原子更新胜利才会进入这里

node 不为 tail 节点且不是 head 节点的后继节点


留神此处的“简单”判断,所有的判断都是为了真正干一件事,把 pred 节点的 next 指针指向 node 节点的后继节点,什么状况下进入该解决逻辑?如下剖析
pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null
1.pred != head 保障了 node 节点不是 head 的后继节点,
2.pred.thread != null 保障了 pred 节点线程还存在,为什么须要?因为可能有另一个线程勾销了 pred 节点导致 pred 节点的 thread 为 null
1.pred.waitStatus == Node.SIGNAL 为 true, 这意味着 node 的无效前驱节点状态为 signal,它会在必要的时候唤醒后继节点,
保障了 node 的无效前驱节点的状态是 SIGNAL

正文完
 0