深刻浅出AQS之共享锁模式

在了解了AQS独占锁模式之后,接下来再来看看共享锁的实现原理。node

原文地址:http://www.jianshu.com/p/1161...segmentfault

搞清楚AQS独占锁的实现原理以后,再看共享锁的实现原理就会轻松不少。两种锁模式之间不少通用的地方本文只会简单说明一下,就不在赘述了,具体细节能够参考个人上篇文章深刻浅出AQS之独占锁模式并发

1、执行过程概述

获取锁的过程:ui

  1. 当线程调用acquireShared()申请获取锁资源时,若是成功,则进入临界区。
  2. 当获取锁失败时,则建立一个共享类型的节点并进入一个FIFO等待队列,而后被挂起等待唤醒。
  3. 当队列中的等待线程被唤醒之后就从新尝试获取锁资源,若是成功则唤醒后面还在等待的共享节点并把该唤醒事件传递下去,即会依次唤醒在该节点后面的全部共享节点,而后进入临界区,不然继续挂起等待。

释放锁过程:线程

  1. 当线程调用releaseShared()进行锁资源释放时,若是释放成功,则唤醒队列中等待的节点,若是有的话。

2、源码深刻分析

基于上面所说的共享锁执行流程,咱们接下来看下源码实现逻辑:
首先来看下获取锁的方法acquireShared(),以下code

public final void acquireShared(int arg) {
        //尝试获取共享锁,返回值小于0表示获取失败
        if (tryAcquireShared(arg) < 0)
            //执行获取锁失败之后的方法
            doAcquireShared(arg);
    }

这里tryAcquireShared()方法是留给用户去实现具体的获取锁逻辑的。关于该方法的实现有两点须要特别说明:队列

1、该方法必须本身检查当前上下文是否支持获取共享锁,若是支持再进行获取。事件

2、该方法返回值是个重点。其1、由上面的源码片断能够看出返回值小于0表示获取锁失败,须要进入等待队列。其2、若是返回值等于0表示当前线程获取共享锁成功,但它后续的线程是没法继续获取的,也就是不须要把它后面等待的节点唤醒。最后、若是返回值大于0,表示当前线程获取共享锁成功且它后续等待的节点也有可能继续获取共享锁成功,也就是说此时须要把后续节点唤醒让它们去尝试获取共享锁。资源

有了上面的约定,咱们再来看下doAcquireShared方法的实现:get

//参数很少说,就是传给acquireShared()的参数
    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);
                    //注意上面说的, 等于0表示不用唤醒后继节点,大于0须要
                    if (r >= 0) {
                        //这里是重点,获取到锁之后的唤醒操做,后面详细说
                        setHeadAndPropagate(node, r);
                        p.next = null;
                        //若是是由于中断醒来则设置中断标记位
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //挂起逻辑跟独占锁同样,再也不赘述
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //获取失败的取消逻辑跟独占锁同样,再也不赘述
            if (failed)
                cancelAcquire(node);
        }
    }

独占锁模式获取成功之后设置头结点而后返回中断状态,结束流程。而共享锁模式获取成功之后,调用了setHeadAndPropagate方法,从方法名就能够看出除了设置新的头结点之外还有一个传递动做,一块儿看下代码:

//两个入参,一个是当前成功获取共享锁的节点,一个就是tryAcquireShared方法的返回值,注意上面说的,它可能大于0也可能等于0
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; //记录当前头节点
        //设置新的头节点,即把当前获取到锁的节点设置为头节点
        //注:这里是获取到锁以后的操做,不须要并发控制
        setHead(node);
        //这里意思有两种状况是须要执行唤醒操做
        //1.propagate > 0 表示调用方指明了后继节点须要被唤醒
        //2.头节点后面的节点须要被唤醒(waitStatus<0),不管是老的头结点仍是新的头结点
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //若是当前节点的后继节点是共享类型获取没有后继节点,则进行唤醒
            //这里能够理解为除非明确指明不须要唤醒(后继等待节点是独占类型),不然都要唤醒
            if (s == null || s.isShared())
                //后面详细说
                doReleaseShared();
        }
    }

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

最终的唤醒操做也很复杂,专门拿出来分析一下:
注:这个唤醒操做在releaseShare()方法里也会调用。

private void doReleaseShared() {
        for (;;) {
            //唤醒操做由头结点开始,注意这里的头节点已是上面新设置的头结点了
            //其实就是唤醒上面新获取到共享锁的节点的后继节点
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //表示后继节点须要被唤醒
                if (ws == Node.SIGNAL) {
                    //这里须要控制并发,由于入口有setHeadAndPropagate跟release两个,避免两次unpark
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;      
                    //执行唤醒操做      
                    unparkSuccessor(h);
                }
                //若是后继节点暂时不须要唤醒,则把当前节点状态设置为PROPAGATE确保之后能够传递下去
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            //若是头结点没有发生变化,表示设置完成,退出循环
            //若是头结点发生变化,好比说其余线程获取到了锁,为了使本身的唤醒动做能够传递,必须进行重试
            if (h == head)                   
                break;
        }
    }

接下来看下释放共享锁的过程:

public final boolean releaseShared(int arg) {
        //尝试释放共享锁
        if (tryReleaseShared(arg)) {
            //唤醒过程,详情见上面分析
            doReleaseShared();
            return true;
        }
        return false;
    }

注:上面的setHeadAndPropagate()方法表示等待队列中的线程成功获取到共享锁,这时候它须要唤醒它后面的共享节点(若是有),可是当经过releaseShared()方法去释放一个共享锁的时候,接下来等待独占锁跟共享锁的线程均可以被唤醒进行尝试获取。

3、总结

跟独占锁相比,共享锁的主要特征在于当一个在等待队列中的共享节点成功获取到锁之后(它获取到的是共享锁),既然是共享,那它必需要依次唤醒后面全部能够跟它一块儿共享当前锁资源的节点,毫无疑问,这些节点必须也是在等待共享锁(这是大前提,若是等待的是独占锁,那前面已经有一个共享节点获取锁了,它确定是获取不到的)。当共享锁被释放的时候,能够用读写锁为例进行思考,当一个读锁被释放,此时不管是读锁仍是写锁都是能够竞争资源的。

相关文章
相关标签/搜索