关于java:JAVA里的锁之二独占锁与共享锁实现分析

3次阅读

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

接着上一篇文章再来剖析下同步是如何实现线程同步的,次要内容有:同步队列,独占式同步状态获取与开释,共享式同步状态获取与开释,超时获取同步状态。

1,同步器状态同步整体阐明
同步器依赖外部的同步队列 (FIFO 双向队列) 业实现同步状态的治理,以后线程获取同步状态失败时,同步器会将以后线程及期待状态等信息结构成一个节点 Node 并将其退出同步队列,同时阻塞以后线程,当同步状态开释时,会将节点中的线程唤醒,使其再次获取同步状态。

同步器蕴含了两个节点类型的援用,一个指向头节点,另一个指向尾节点。获取同步状态时只会有一个线程能获取胜利,因而设置头节点时不须要应用 CAS,只须要将首节点设置成为原首节点的后继节点并断开原首节点的 next 援用。首节点的线程在开释同步状态时,会唤醒后继节点,后继节点在获取到同步状态后将本人设置为首节点。

其它没获取到头节点的线程会被结构成节点退出到同步队列的尾部,因为有多个线程所以应用了 CAS 设置尾节点。

2,同步器源码阐明

2.1 Node 节点类

同步队列中的节点 (Node) 用来保留获取同步状态失败的线程援用,期待状态及前驱和后继节点。

    static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {return nextWaiter == SHARED;}

        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

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

waitStatus,期待状态,蕴含如下状态:

CANCELLED(1):因为在同步队列中期待的线程超时或被中断须要从同步队列中勾销期待,节点进入该状态将不会变动
SIGNAL(-1):后继的线程处于期待状态,如果以后节点的线程开释了同步状态或者被勾销,将会告诉后继节点使后继节点的线程得以运行。CONDITION(-2):节点处于期待队列中,节点线程期待在 Condition 上,当其它线程对 Condition 调用了 signal 后,该节点将会从期待队列中转移到同步队列,退出到对同步状态的获取中。PROPAGATE(-3):示意下一次共享式同步状态获取将会无条件被流传上来。INITIAL(0): 初始状态。留神,负值示意结点处于无效期待状态,而正值示意结点已被勾销。所以源码中很多中央用 >0、<0 来判断结点的状态是否失常。

Node prev,前驱节点,当节点退出同步队列时被设置(尾部增加)。
Node next,后继节点。
Node nextWaiter,期待队列中的后继节点,如果以后节点是共享的,那么这个字段是一个 SHARED 常量,也就是说节点类型(分独占和共享)和期待队列中的后继节点共用一个字段。
Thread thread,获取同步状态的线程。

3,独占锁及共享锁的实现

3.1.1 acquire(int)

此办法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程间接返回,否则进入期待队列,直到获取到资源为止,且整个过程疏忽中断的影响。这也正是 lock()的语义,当然不仅仅只限于 lock()。获取到资源后,线程就能够去执行其临界区代码了。上面是 acquire()的源码:

    public final void acquire(int arg) {if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();}

流程如下:

  1. tryAcquire()尝试间接去获取资源,如果胜利则间接返回(&& 为短路运算符,这里体现了非偏心锁,每个线程获取锁时会尝试间接抢占加塞一次,而 CLH 队列中可能还有别的线程在期待);如果失败则进一步执行 acquireQueued()
  2. addWaiter()将该线程退出期待队列的尾部,并标记为独占模式。
  3. acquireQueued()使线程阻塞在期待队列中获取资源,始终获取到资源后才返回。如果在整个期待过程中被中断过,则返回 true,否则返回 false。
  4. 如果线程在期待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断 selfInterrupt(),将中断补上。
    protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();
    }

AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)。

    private Node addWaiter(Node mode) {
        // 以给定模式结构结点。mode 有两种:EXCLUSIVE(独占)和 SHARED(共享)Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        // 尝试疾速在尾部增加
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //CAS 自旋退出队尾
        enq(node);
        return node;
    }
    // 退出队尾
    private Node enq(final Node node) {
        //CAS"自旋",直到胜利退出队尾
        for (;;) {
            Node t = tail;
            if (t == null) {// 队列为空,创立一个空的标记结点作为 head 结点,并将 tail 也指向它。if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {// 放入队尾
                    t.next = node;
                    return t;
                }
            }
        }
    }
    // 获取同步状态
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;// 标记是否胜利拿到资源,true 未获取;false 已获取
        try {
            boolean interrupted = false;// 标记期待过程中是否被中断过
            for (;;) {final Node p = node.predecessor();// 前驱节点
                // 如果前驱节点是头节点,才有资格 (可能是被头节点唤醒或被中断) 去获取同步状态
                if (p == head && tryAcquire(arg)) {
                    // 将同步器节点指向以后节点,以后节点也就成为了头节点
                    setHead(node);
                    // 将之前的头节点的 next 置为 null,也就是断开与下一节点的连贯
                    // 这个操作是将之前的头节点从队列里移除了
                    p.next = null;//HELP GC
                    failed = false;// 胜利获取资源
                    return interrupted;
                }
                // 如果前驱节点不是头节点且尝试获取同步状态失败,则阐明须要 park
                // 从而进入 waiting 状态直到被 unpark()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果期待过程中没有胜利获取资源(如 timeout,或者可中断的状况下被中断了),那么勾销结点在队列中的期待。if (failed)
                cancelAcquire(node);
        }
    }
    /*
     * 此办法次要用于查看状态,看看本人是否真的能够去劳动了,* 免得队列前边的线程都放弃了而本人始终盲等。* 整个流程中,如果前驱结点的状态不是 SIGNAL,那么本人就不能安心去劳动,* 须要去找个安心的劳动点,同时能够再尝试下看有没有机会轮到本人拿号
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;// 获取前驱节点状态
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             * 如果曾经通知前驱拿完号后告诉本人一下,那就能够安心劳动了
             * 能够看下下面 2.1 Node 类里对于 waitStatus 的阐明
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             * 前驱节点状态大于 0 阐明前驱节点因为期待超时或被中断放弃了期待
             * 如果前驱放弃了,那就始终往前找,直到找到最近一个失常期待的状态,* 并排在它的后边。* 留神:那些放弃的结点,因为被本人“加塞”到它们前边,* 它们相当于造成一个无援用链,稍后就会被 GC 回收
             */
            do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             * 如果前驱失常,那就把前驱的状态设置成 SIGNAL,通知它拿完号后
             * 告诉本人一下。*/
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    /*
     * 如果线程找好平安劳动点后,那就能够安心去劳动了。此办法就是让线程去劳动,* 真正进入期待状态。*/
    private final boolean parkAndCheckInterrupt() {//park()会让以后线程进入 waiting 状态。在此状态下,有两种路径能够唤醒该线程:1)被 unpark();2)被 interrupt()。LockSupport.park(this);
        //Thread.interrupted()会革除以后线程的中断标记位。return Thread.interrupted();}

这里总结下 acquireQueued()办法的流程:

  1. 结点进入队尾后,查看状态,找到平安劳动点;
  2. 调用 park()进入 waiting 状态,期待 unpark()或 interrupt()唤醒本人;
  3. 被唤醒后,看本人是不是有资格能拿到号。如果拿到,head 指向以后结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,持续流程 1。

acquire()办法也做一个总结:

  1. 调用自定义同步器的 tryAcquire()尝试间接去获取资源,如果胜利则间接返回;
  2. 没胜利,则 addWaiter()将该线程退出期待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在期待队列中劳动,有机会时(轮到本人,会被 unpark())会去尝试获取资源。获取到资源后才返回。如果在整个期待过程中被中断过,则返回 true,否则返回 false。
  4. 如果线程在期待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断 selfInterrupt(),将中断补上。

3.1.2 release(int)

该办法为开释同步状态,在开释了之后还会唤醒其后继节点,这样后继节点才有机会从新尝试获取同步状态。

    public final boolean release(int arg) {if (tryRelease(arg)) {
            Node h = head;// 找头节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);// 唤醒下一节点
            return true;
        }
        return false;
    }
    /*
     * 跟 tryAcquire()一样,这个办法是须要独占模式的自定义同步器去实现的
     */
    protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();
    }

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         * ws 为以后节点的状态,此时 node 还是头节点
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);// 将本身状态置为 0

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;// 下一节点
        // 如果为空或期待的节点状态已有效(期待超时或被中断,须要从队列里勾销期待)if (s == null || s.waitStatus > 0) {
            s = null;
            // 从后往前找,waitStatus 小于 0 的期待是无效的
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);// 将找到的适合节点唤醒
    }

总结一下 release(int)办法就是:

一句话概括,用 unpark()唤醒期待队列中最前边的那个未放弃线程

下面剖析了独占锁的获取与开释,上面来看下共享锁的获取与开释。

3.2.1 acquireShared(int)

共享式获取与独占式获取最次要区别在于同一时刻是否有多个线程同时获取同步状态。

    public final void acquireShared(int arg) {
        /*
         *tryAcquireShared 仍然须要子类去实现,但返回值的含意已定义好:* 负值代表获取失败;* 0 代表获取胜利,但没有残余资源;* 负数示意获取胜利,还有残余资源,其余线程还能够去获取
         */
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

    private void doAcquireShared(int arg) {
        // 结构共享节点并退出到队尾
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {final Node p = node.predecessor();// 前驱节点
                if (p == head) {int r = tryAcquireShared(arg);// 尝试获取资源
                    if (r >= 0) {// 获取胜利
                        // 将 head 指向本人,还有残余资源能够再唤醒之后的线程
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        // 如果期待过程中被打断过,此时将中断补上
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 判断状态,寻找平安点,进入 waiting 状态,等着被 unpark()或 interrupt()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {if (failed)
                cancelAcquire(node);
        }
    }
    // 将本人设置为头节点,如果还有残余资源还会唤醒下一邻近节点
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         * 如果还有残余量,持续唤醒下一个街坊线程
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();}
    }

与独占模式相比,共享式获取到资源后,如果有残余资源还会去唤醒后继节点。

3.2.2 releaseShared()

如果胜利开释且容许唤醒期待线程,它会唤醒期待队列里的其余线程来获取资源。

    public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {// 尝试开释资源,须要子类实现
            doReleaseShared();// 唤醒后继节点
            return true;
        }
        return false;
    }

    private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue; // loop to recheck cases
                    unparkSuccessor(h);// 唤醒后继
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;// loop on failed CAS
            }
            if (h == head)// loop if head changed
                break;
        }
    }

这里再提一下对于超时获取资源贴一下代码:

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {if (nanosTimeout <= 0L)
            return false;
        // 截止工夫
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);// 独占式
        boolean failed = true;
        try {for (;;) {final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {// 尝试获取
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                // 减去尝试获取所花工夫
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)// 工夫到了或已超时,须要立刻返回
                    return false;
                // 获取失败后须要期待,且须要期待的工夫仍大于 1s
                // 则期待 nanosTimeout
                // 阐明下,如果需等待时间已小于 1s 也进入期待的话,那通过下一轮的
                // 计算误差会很大,所以只有大于 1s 才进入期待。if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();}
        } finally {if (failed)
                cancelAcquire(node);
        }
    }

好了,这里剖析完了独占锁与共享的获取与开释,下一篇文章再剖析下重入锁,读写锁及锁降级相干的知识点。

参考的文章:
Java 并发之 AQS 详解
积淀再登程:对于 java 中的 AQS 了解

正文完
 0