【Java并发编程】详细分析AQS原理之共享锁

        上一篇我们详细分析了AQS的原理和独占式获取的方式,现在先来回顾一下AQS的基本思想:

        1.操作与规则分离:AQS实现了了同步状态的管理,线程的排队,等待与唤醒等底层操作,而把线程能否获取资源,如何获取资源等业务规则则交由子类实现

        2.使用CLH队列来管理等待获取同步状态的线程。

        AQS定义两种资源访问方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。本篇将讲解AQS对共享锁的实现!

一:AQS共享式获取思想

        (1)多线程同时获取锁

        共享式与独占式获取的最主要区别在于,共享式获取支持多个线程同时拥有锁,而独占式获取在任意时刻,最多只有一个线程获取到锁。比如当一个程序经常进行读取操作,而写操作确很少时,那么就需要对读线程使用共享锁,保证最大的读取量,而对写线程使用独占锁。

        (2)顺序共享

        考虑这样一个问题:假设总共有4个资源,在同步队列中有三个线程共享获取同步状态,如下图所示,

        A使用2个,B需要3个,C需要1个,那么C会比B先抢夺同步状态吗?

        答案是:同步队列中的线程会严格按照FIFO的顺序进行获取共享锁,只有等B获取锁之后,C才能获取共享锁。

二:执行流程

        锁的获取过程:

        1.先使用tryAcquireShared尝试获取共享锁,如果获取成功直接返回,进入临界区。

        2.获取失败,将当前线程包装成共享类型节点加入同步队列,然后自旋,等待获取共享锁。

        3.如果在队列中获取锁成功,就去唤醒后面还在等待获取共享锁的共享节点,直到遇到独占节点,或者资源不足。

        锁释放过程:

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

三:源码分析

锁的获取

        通过上面对锁获取流程的介绍,下面就通过源码分深入的分析锁的获取过程。

       首先看一下acquireShared方法:

public final void acquireShared(int arg) {
//1.使用子类实现的tryAcquireShared方法尝试获取共享锁
        if (tryAcquireShared(arg) < 0)
//2.共享锁获取失败,就将当前线程加入同步队列,等待获取共享锁。
            doAcquireShared(arg);
    }

         其中tryAcquireShared是由子类实现的,用来实现获取共享锁。

        值得注意的是:该方法的返回值如果大于0表示当前线程获取锁成功,并且还有同步资源剩余,需要唤醒后继的节点来尝试获取共享锁。如果等于0表示当前线程获取共享锁成功,但它后续的线程是无法继续获取的,也就是不需要把它后面等待的节点唤醒。如果小于0表示当前线程获取锁失败。

        下面来看一下doAcquireShared方法:

private void doAcquireShared(int arg) {
//1.将当前线程包装成共享节点,加入到同步队列中
        final Node node = addWaiter(Node.SHARED);
//2.失败标记
        boolean failed = true;
        try {
//3.中断标记
            boolean interrupted = false;
            for (;;) {
//4.自旋
                final Node p = node.predecessor();
//5.如果当前节点的前驱是头节点,使用tryAcqureShared方法尝试获取共享锁。
                if (p == head) {
                    int r = tryAcquireShared(arg);
//6.如果tryAcqureShared返回值>=0,表示获取共享锁成功,使用setHeadAndPropagate设置当前节点为
//头节点,并唤醒后面的共享节点。这里就是共享的概念,也是和独占获取方式最大的区别。
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
//7.如果获取失败,就设置并判断前驱节点的waitStatus状态,睡眠等待唤醒
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
//8。如果获取锁失败,就将当前节点标记为取消节点。
            if (failed)
                cancelAcquire(node);
        }
    }

下面看setHeadAndPropagate的源码:

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
//1.propagate>0 表示还有剩余资源,需要唤醒后继共享节点
//2.h.waitStatus<0 如果h.waitStatus = PROPAGATE,表示之前的某次调用暗示了资源有剩余,所以需要
//唤醒后继共享模式节点,由于PROPAGATE状态可能转化为SIGNAL状态,所以直接使用h.waitStatus < 0来判断
//如果现在的头节点的waitStatus<0,唤醒
//3.h==null,表示此节点变成头节点之前,同步队列为空,现在当前线程获得了资源,那么后面共享的节点也
//可能获得资源
//以上3种情况可以看出,非常的保守,可能导致多次不必要的唤醒。
        if (propagate > 0 || h == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

这里详细说一下第二点: 如果h.waitStatus = PROPAGATE,表示之前的某次调用暗示了资源有剩余,所以需要唤醒后继共享模式节点,由于PROPAGATE状态可能转化为SIGNAL状态,所以直接使用h.waitStatus < 0来判断。如果现在的头节点的waitStatus<0,唤醒。

假设:资源总数为8,线程A使用了4个资源,同步队列中的线程B正在使用2个资源,线程C需要2个资源,且此时也获得资源成功,那么线程C执行tryAquireShared方法时,就会返回0,表示获取同步资源成功,但是没有多余的资源,不需要唤醒后继共享节点。如下图所示:

【Java并发编程】详细分析AQS原理之共享锁_第1张图片

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

下面来看doReleaseShared方法的源码:

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

1.AQS等待队列节点的PROPAGATE状态代表唤醒的行为需要传播下去,当头节点的后继节点并未处于阻塞状态时(可能是刚调用addWaiter方法添加到队列中还未来得及阻塞),就给头节点设置这个标记,表示下次调用setHeadAndPropagate函数时会把这个唤醒行为传递下去。
2.设置PROPAGATE状态的意义主要在于,每次释放permit都会调用doReleaseShared函数,而该函数每次只唤醒等待队列的第一个等待节点。所以在本次归还的permit足够多的情况下,如果仅仅依靠释放锁之后的一次doReleaseShared函数调用,可能会导致明明有permit但是有些线程仍然阻塞的情况。所以在每个线程获取到permit之后,会根据剩余的permit来决定是否把唤醒传播下去。但不保证被唤醒的线程一定能获得permit。
3.共享模式下会导致很多次不必要的唤醒。

锁的释放

public final boolean releaseShared(int arg) {
//1.使用子类实现的tryReleaseShared方法尝试释放资源,如果释放成功,就调用doReleaseShared方法
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

可以看到,当重写的tryReleaseShared(arg)方法返回true,成功释放锁资源,进入doReleaseShared()唤醒等待的线程,这个方法上面已经分析过,这里不再赘述。


参考:https://blog.csdn.net/Q_AN1314/article/details/79895128 

你可能感兴趣的:(java并发编程)