AbstractQueuedSynchronizer共享锁源码分析

前言

上一篇文章中,我们分析了AbstractQueuedSynchronizer独占锁的源码。
没看的建议先移步上一篇,共用的代码这篇文章中不会再进行详细的解释。接下来,会继续把学习AbstractQueuedSynchronizer共享锁的过程记录下来,希望对你们有所帮助。如果文章中有错误的地方,也请在评论区不吝指正,本人也将第一时间修改错误,来帮助更多的人。

共享同步器的原理

共享锁的实现和同步锁稍有不同,共享锁是对一组资源的控制,打个通俗的比方。学校器材室有10个篮球,每个班级都有权利来借取篮球去使用,使用完毕后归还。有一天大家上体育课,一班的同学跑得快,先到器材室跟大爷借了6个篮球,二班赶到之后,二班需要7个篮球,发现不够,于是在器材室门口排队等候一班归还篮球。这时候三班四班也来借篮球,三班需要2个,四班需要3个。这个时候大家可能会想,现在还剩下4个篮球。可以给三班或者给四班先使用,想法很好,但是老大爷可不允许。老大爷规定,先来后到,不够就等着,于是二三四班都在门口等着,过了一会,一班把篮球归还,还完之后告诉二班我还球了,二班发现需要的7个够了,于是二班借走了七个球,借走之后发现还有余量,于是二班告诉了三班,三班也去借球,借到了两个,发现还有余量,于是告诉4班,4班去接球,发现只有1个了。不够自己使用,于是继续等待。这就是共享锁的基本机制, 细心的可以发现,等待队列严格遵循FIFO, 这其实是保证了公平性, 但是降低了并发量。因为一班一直不还球的话,三班四班明明可以先借到球去玩,但是因为二班需要的球不够,也跟着一起等待。

共享同步器的简单实现

共享同步器,只需要实现tryAcquireShared(int arg)和tryReleaseShared(int arg)就可以。这里借鉴了一下Semaphore的实现

public class MyShardLock {

    private final MySync mySync;

    public MyShardLock(int permits) {
        this.mySync = new MySync(permits);
    }
    
    public void acquire(int permits) {
        if (permits < 0) {
            throw new IllegalArgumentException();
        }
        mySync.acquireShared(permits);
    }
    
    public void release(int permits) {
        mySync.releaseShared(permits);
    }

    final class MySync extends AbstractQueuedSynchronizer {
        MySync(int permits) {
            setState(permits);
        }

        @Override
        protected int tryAcquireShared(int arg) {
            for (; ; ) {
                int available = getState();
                int remaining = available - arg;
                if (remaining < 0 ||
                        compareAndSetState(available, remaining)) {
                    return remaining;
                }
            }

        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            for (; ; ) {
                int current = getState();
                int next = current + arg;
                if (next < current) {
                    throw new Error("Maximum permit count exceeded");
                }
                if (compareAndSetState(current, next)) {
                    return true;
                }
            }
        }
    }

}

这里需要注意的一点是,tryAcquireShared(int arg)不再像独占锁返回的Boolean值,而是返回的int值:

  1. 返回负数, 代表资源不够,无法获得资源。
  2. 返回零, 代表资源分配后刚好够,资源已分配。
  3. 返回正数, 代表资源分配后还有余量, 资源同样已分配。

源码分析

还是从加锁开始, 加锁首先调用的是acquireShared(int arg)

acquireShared(int arg)

	public final void acquireShared(int arg) {
		// 首先去获取资源,获取成功直接返回,不成功才会进入doAcquireShared(arg)
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

首先调用tryAcquireShared(arg),这个同样需要自己去实现

	protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
	@Override
       protected int tryAcquireShared(int arg) {
           for (; ; ) {
               int available = getState();
               int remaining = available - arg;
               if (remaining < 0 ||
                       compareAndSetState(available, remaining)) {
                   return remaining;
               }
           }

       }

我们的实现里,计算资源数量,不够直接返回,加自旋是为了保证cas一定能成功。
如果资源不够,则去调用doAcquireShared(arg)

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) {
                    	// 申请成功后逻辑,下面单讲
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        // 补充自我中断逻辑,独占锁的是放在acquire()里,其实都一样
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 这里的逻辑上一篇已经细讲过,这里不再赘述,需要的去看上一篇
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
            	// 这个上一篇里也有。
                cancelAcquire(node);
        }
    }

需要注意的两点

  1. 补充中断的时机,独占锁里补充中断是放在acquire()里,这里是放在了自旋逻辑里。我认为都是一样的。如果有我没注意到的细节,欢迎留言告诉我。
  2. finally的执行,和独占锁一样,也是自定义实现里出异常才会执行。

setHeadAndPropagate(Node node, int propagate)

这是申请资源成功后调用的方法

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

这里我们看下setHeadAndPropagate(Node node, int propagate)方法,参数propagate是tryAcquireShared(arg)返回的值。逻辑只有一个,满足条件触发doReleaseShared()。注意,前面两个判断的是旧head,后两个判断是新head。这个判断最后会讲,因为涉及到锁的释放。

这里先讲一下锁的释放,因为锁释放也调用了doReleaseShared()

	public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

tryReleaseShared(arg)方法没什么好说的,看一下上面的实现就懂了。释放成功就会调用doReleaseShared()。这里和独占锁不同的一点是,独占锁直接就可以释放资源,因为有且只有一个线程能获取到资源, 而共享锁不一样 ,共享锁可以同时存在好几个线程拿到资源,所以共享锁释放线程要使用乐观锁。

	@Override
       protected boolean tryReleaseShared(int arg) {
           for (; ; ) {
               int current = getState();
               int next = current + arg;
               if (next < current) {
                   throw new Error("Maximum permit count exceeded");
               }
               if (compareAndSetState(current, next)) {
                   return true;
               }
           }
       }

doReleaseShared()

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

这里释放资源的时候,检测头结点的状态,如果是SIGNAL,则去唤醒下一个线程。SIGNAL状态很好理解,因为大多数等待的Node都是这个状态,unparkSuccessor(h)方法不再赘述,这里说一下else if里的逻辑。ws什么时候会是0呢?

  1. 新增的节点,新增的节点是0,但是新增的节点必然是尾节点,上面第一个if判断里,有一个h != tail的判断,所以这个地方存在冲突。但是一定冲突吗?还真不一定。我们做一个这样的假设, 假设线程A获取资源,并且直接获取成功,而队列还没有初始化。此时,线程2来获取资源, 没有获取到,于是走到addWaiter(Node.SHARED)方法里的enq()方法去初始化队列,当走到if (compareAndSetHead(new Node()))和tail = head之间的时候。这个时候头结点是个dummy node,符合head != null, 而这个时候尾节点还没有初始化,是null,同时满足h != tail, 所以如果此时线程A去释放资源,就有可能走到ws == 0这个判断里。

  2. 唤醒下一个节点的时候,会先把自身的状态设置为0。假设线程A获取资源成功,并且还有余量,就会进入到doReleaseShared()方法继续唤醒下一个Node, 唤醒下一个节点之前,把ws从SIGNAL变更为0, 下一个节点线程B去获取资源,此时有两种情况,
    情况1:线程B还没有尝试获取资源,线程A去释放资源,再次走到doReleaseShared()里,此时head节点即线程A状态便是0,把状态变更为PROPAGATE,线程B去获取资源成功,线程A以PROPAGATE状态结束
    情况2:线程B尝试获取资源但是失败,还没有走到if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())这个判断,此时线程A去释放资源,再次走到doReleaseShared()里,此时head节点即线程A状态便是0,把状态变更为PROPAGATE,线程B走到shouldParkAfterFailedAcquire()方法里把线程A状态变更为SIGNAL,然后再次自旋tryAcquireShared(arg)成功,因为线程A已经释放了资源, 线程A以SIGNAL状态结束
    如果还有其他情况,欢迎留言补充!

我们再回顾一下这段代码

	private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

这五个条件我们逐一分析,首先明确一点,这五个条件是或链接,如果走到后面的判断,那么前面的判断必为false。

  1. propagate > 0, 这个条件一目了然,就是申请资源后,还有支援的剩余。
  2. h == null, 这个条件大家可能就有点迷糊了。什么情况下,h==null呢。翻阅代码我发现,h == null不可能成立, 因为Node h = head在执行setHead()之前,这个时候head肯定不为null,即使队列之前为空走了enq()方法,也会有一个dummy node,我网上翻阅资料后,发现这个是防止空指针异常发生的标准写法。
  3. h.waitStatus < 0,这个h是旧head, 这里有两种情况:
    情况一:我们假设一个线程B在doAcquireShared(int arg)方法里刚执行完图下的时间片1,另一个线程A是尾节点,且去释放资源,走到doReleaseShared()里,这个时候ws就是0,状态被设置为Node.PROPAGATE。然后线程B获取资源,由于资源已经释放,肯定可以获取到,资源获取到之后走到setHeadAndPropagate()方法,这个条件就会成立了。但是这是一次无效的唤醒。因为走到这个判断,propagate必然等于0。这时,Node.PROPAGATE只是一个中间状态,方便被检测到。
    情况二:还一种情况是一个线程A在doAcquireShared(int arg)方法里走到了图下时间片2。另一个线程B释放资源,状态设置为Node.PROPAGATE, 线程A走到shouldParkAfterFailedAcquire()方法里,又把头结点状态ws更改为SIGNAL, 之后在第二次执行自旋的tryAcquireShared(arg)获取到了资源,走到setHeadAndPropagate()方法, 此时这个条件仍然是成立的,这仍然是一次无效的唤醒。
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED); 
        // 时间片1
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 时间片2
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  1. (h = head) == null, 这个也不会成立的。同二
  2. h.waitStatus < 0 ,这个是经常存在的,因为只要有后继节点,后继节点就会把前驱节点的状态设置为SIGNAL, 一个线程拿到资源时,如果有后继节点,就会走到这个判断。这种情况也会造成不必要的唤醒

关于不必要的唤醒

作者在注释里提到了,不必要的唤醒是有必要的。这段代码经过这么久的测试,也确实证明了是很健壮的。

		* 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.

至此,AbstractQueuedSynchronizer的部分已经讲完了,我们更多的应该去思考。很多时候,思考的可能都是错误的,但是这种思考的过程很重要,如果能对思考的结果做一个求证,那会更加的完美。

关于学习AbstractQueuedSynchronizer引申的几个思考

看到这里,我觉得大家有必要问自己几个问题。

  1. AbstractQueuedSynchronizer是公平锁还是非公平锁?
  2. 学习AbstractQueuedSynchronizer有什么用?或者可以说学习了AbstractQueuedSynchronizer让我们对jdk哪些常用的工具类一目了然了。
  3. AbstractQueuedSynchronizer源码中有什么值得我们学习的地方并且可以应用在自己的编码里的。
  4. 自己能不能写一篇博客来给大家讲一讲AbstractQueuedSynchronizer。

关于这几个问题,相信大家都有不同的答案,还是那句话,思考很重要。我来给大家说一说第四个问题的,源码我看过不少,相信大家也都或多或少的看过,但是看过之后马上就忘掉的居多,看着看着瞌睡的时候也不少。我在看ThreadPoolExecutor的时候,一个封装任务的Worker类继承了aqs,我当时突然想到一个问题,aqs的源码之前我看过一次,现在如果让我跟别人讲一讲,我能不能讲出来。答案是否定的。然后我就问自己,为什么我会遗忘的这么快。我想起了我小学语文老师说过的一句话,看一遍不如写一遍,写一遍不如给别人讲一遍。于是我开始了写博客之旅,记录学习,分享知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值