转载

JDK锁的基础--AQS实现原理(三)

本文主要来分析一下AQS共享模式锁的获取和释放,AQS其实只是一个框架,它主要提供了一个int类型的state字段,子类继承时用于存储子类的状态,并且提供了一个等待队列以及维护等待队列的方法。至于如何使用这个状态值和等待队列,就需要子类根据自己的需求来实现了。

以Semaphore类为例,Semaphore允许多个线程同时获得信号量先来看一下Semaphore的接口:

//Semaphore
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

同样的,sync是一个定义在Semaphore中的AQS的抽象子类,在Semaphore类中有两种实现,一个是公平的,一个是非公平的。转到AQS中的 acquireSharedInterruptibly 方法,

//AbstractQueuedSynchornizer
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //由于本文分析共享模式锁,所以说tryAcquireShared尝试获取的是permit而不是锁
        //tryAcquireShared尝试获取相应数量的permit,如果失败返回负值。返回0代表获取成功但是下次调用会失败,返回正值代表获取成功而且下次调用可能也会成功
        //可以理解为返回0代表只有0个permit,所以下次调用会失败,而返回正值代表还有permit,所以下次调用可能会成功
        if (tryAcquireShared(arg) < 0)
            //获取失败后需要新建一个等待节点并将节点加入等待队列
            doAcquireSharedInterruptibly(arg);
    }

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //新建一个共享模式的节点并将其加入等待队列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    //如果其前驱节点是头节点,那么再次尝试获取permit
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        //如果获取成功那么将该节点设置成头节点,并且如果r>0,代表还有剩余的permit,所以如果该节点的后继节点也是共享模式的,就把后继节点也唤醒
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

来看一下 setHeadAndPropagate 方法,这个方法和 setHead 不同的地方在于它不仅设置了等待队列的头节点,并且检查其后继节点是否 可能 是共享模式节点,如果是,而且传入的 propagate 大于0或者头节点设置了 PROPAGATE 状态,那么需要调用 doReleaseShared 方法来唤醒后继节点。 setHeadAndPropagate 方法的处理过程比较保守,可能会导致很多不必要的唤醒。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        //如果propagate>0,代表有剩余的permit,唤醒共享模式节点
        //如果h.waitStatus = PROPAGATE,表示之前的某次调用暗示了permit有剩余,所以需要唤醒共享模式节点
        //由于PROPAGATE状态可能转化为SIGNAL状态,所以直接使用h.waitStatus < 0来判断
        //如果现在的头节点的waitStatus<0,唤醒
        //如果现在的头节点等于null,唤醒
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //如果后继节点为null,whatever唤醒
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

可以看到 setHeadAndPropagate 方法的原则是宁滥勿缺,反正 doReleaseShared 方法会继续后来的处理:

private void doReleaseShared() {      
        for (;;) {
            Node h = head;
            //如果头节点不为空且头节点不等于尾节点,亦即等待队列中有线程在等待
            //需要注意的是,等待队列的头节点是已经获得了锁的线程,所以如果等待队列中只有一个节点,那就说明没有线程阻塞在这个等待队列上
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    //如果头节点的状态是SIGNAL,代表需要唤醒后面的线程(SIGNAL状态可以看做是后继节点处于被阻塞中)
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //唤醒后继节点
                    unparkSuccessor(h);
                }
                //如果头节点的状态为0,说明后继节点还没有被阻塞,不需要立即唤醒
                //把头节点的状态设置成PROPAGATE,下次调用setHeadAndPropagate的时候前任头节点的状态就会是PROPAGATE,就会继续调用doReleaseShared方法把唤醒“传播”下去
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            //如果头节点被修改了那么继续循环下去
            if (h == head)                   // loop if head changed
                break;
        }
    }

根据自己的思考总结一下,不保证正确性:

  1. AQS的等待队列的头节点在初始化的时候是个哑节点,其它时候代表已经获取锁的节点(独占模式)或者获取了permit的节点(共享模式),设置了头节点的线程已经可以执行临界区代码了。也就是说,在共享模式下,获得了permit的线程代表的节点可能被其它节点挤出等待队列。总之,等待队列从第二个节点开始才是正在等待的线程。
  2. AQS的等待队列的节点类Node只有在其后继节点被阻塞的情况下才会是 SIGNAL 状态,所以 SIGNAL 状态代表其后继节点正在阻塞中。
  3. AQS等待队列节点的 PROPAGATE 状态代表唤醒的行为需要传播下去,当头节点的后继节点并未处于阻塞状态时(可能是刚调用 addWaiter 方法添加到队列中还未来得及阻塞),就给头节点设置这个标记,表示下次调用 setHeadAndPropagate 函数时会把这个唤醒行为传递下去。
  4. 设置 PROPAGATE 状态的意义主要在于,每次释放permit都会调用 doReleaseShared 函数,而该函数每次只唤醒等待队列的第一个等待节点。所以在本次归还的permit足够多的情况下,如果仅仅依靠释放锁之后的一次 doReleaseShared 函数调用,可能会导致明明有permit但是有些线程仍然阻塞的情况。所以在每个线程获取到permit之后,会根据剩余的permit来决定是否把唤醒传播下去。但不保证被唤醒的线程一定能获得permit。
  5. 共享模式下会导致很多次不必要的唤醒。
原文  https://blog.csdn.net/Q_AN1314/article/details/79895128
正文到此结束
Loading...