J.U.C包核心AQS(三):同步状态的获取与释放(独占/共享)

看过该系列第一篇文章的朋友相信还记得,对于同步状态的获取与释放,与我们自定义同步器时重写的tryAcquire()/tryRelease()(独占式)或者是tryAcquireShared()/tryReleaseShared()(共享式)有关。

因为 AQS 是基于模板方法设计模式设计的,线程获取锁时,由内部的同步器去调用 AQS 实现好的acquire()/acquireShared()方法,然后这些方法去调用我们刚才所说的自己重写的方法。释放锁时也是如此。

一般来说,自定义同步器要么是独占模式,要么是共享模式,他们也只需实现tryAcquire()/tryRelease()tryAcquireShared()/tryReleaseShared()中的一对即可(所以这些方法都未被 abstract 修饰)。但 AQS 也支持自定义同步器同时实现独占和共享两种模式,比如 ReentrantReadWriteLock,它是一个可重入的读写锁,读锁共享,写锁独占。

本文将详细的介绍同步状态的获取与释放的完整过程,并且是基于独占与共享两种模式。

 

一、独占模式

1. 独占式获取同步状态

首先来看acquire(),该方法对中断不敏感,也就是说,当线程获取同步状态失败后进入同步队列中,如果后续对线程进行中断操作的话,该线程不会从同步队列中移出。

AQS 还提供了acquireInterruptibly()/tryAcquireNanos()方法,分别是响应中断式的获取同步状态与超时获取同步状态,不过归根结底都是基于acquire()方法的思想,掌握了acquire()方法,其它的也都很容易理解了,所以本文将详细介绍acquire()方法的调用流程,至于上述两个增强版的方法,就由读者自行去体会了。

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

它首先会调用tryAcquire(),而这个方法是自定义同步器重写的方法,我们之前有介绍过,它用来去获取独占资源。如果获取成功,则直接返回 true,否则直接返回 false。

由于不同同步器的实现方式不同所以这里不过多的介绍tryAcquire(),不过我会在接下来的该系列的其他文章中梳理到 ReentrantLock 时,详细的介绍 ReentrantLock 重写的tryAcquire()方法。

acquire()的源码可以得知,如果调用tryAcquire()方法返回 true,那么方法直接执行完成,不会执行后续的acquireQueued()方法,这是由于短路与的特性。

那么如果tryAcquire()方法返回 false,此时线程获取同步状态失败,便会执行acquireQueued()方法,并且我们看到,它会先执行addWaiter()方法,这个方法我在该系列的上一篇文章当中曾详细的介绍过,它是向同步队列的尾部添加节点时执行的函数,这里不再赘述,忘记的朋友可以返回去看看。

我们接下来主要看看acquireQueued()方法

   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;// 获取同步状态失败了吗?
        try {
            boolean interrupted = false;// 中断标志
            //死循环,但是自旋次数很少!
            for (;;) {
                final Node p = node.predecessor();// 获取当前节点的前驱节点
                // 如果当前节点的前驱结点为头节点,并且获取同步状态成功,执行下面逻辑
                if (p == head && tryAcquire(arg)) {
                    setHead(node);// 将当前节点设置为头节点,这里不需要CAS,自己想想为什么
                    p.next = null; // 在setHead方法中,node.prev被置为null,这里又将p.next被置为null,很显然是为了GC
                    failed = false;
                    return interrupted;// 返回等待过程中线程是否被中断过
                }
                // 这块很关键,判断线程是否需要等待
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued()方法我在注释里写的够详细了,这里不多说了,看注释吧,我们接下来着重关注一下 shouldParkAfterFailedAcquire()parkAndCheckInterrupt()这两个方法。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;// 获取前驱结点的等待状态
        if (ws == Node.SIGNAL)
            /*
             * 前驱节点已经被设置成了SIGNAL状态,说明前驱节点释放后会通知当前节点
             * 那么这时当前节点所持有的线程就可以安安心心的去等待了
             */
            return true;
        if (ws > 0) {
            /*
             * 如果前驱节点的waitStatus大于0,说明它的前驱节点已经放弃等待了
             * 或是超时或是被中断,那么此时,当前节点就需要一直往前找,直到找到正常的节点
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 使用CAS来设置前驱节点的waitStatus为SIGNAL
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);// 阻塞当前线程,使线程进入waiting状态
        return Thread.interrupted();// 返回中断标记
    }

结合 acquireQueued()shouldParkAfterFailedAcquire()parkAndCheckInterrupt()这三个方法我们会发现,只要当前节点的前驱节点的等待状态不是 SIGNAL,那么线程就处于自旋的过程中,直到它的前驱节点的等待状态是 SIGNAL 了。

那么如果在shouldParkAfterFailedAcquire()方法发现当前节点的前驱节点的等待状态是 SIGNAL 了,那么此时就可以执行parkAndCheckInterrupt()了,它会调用LockSupport.park(this) 阻塞 当前线程,使当前线程进入等待(waiting)状态。

这里我为什么标红呢,因为这个挺有意思的,熟悉 J.U.C 的朋友应该都知道,J.U.C 实现阻塞/唤醒线程是通过 LockSupport 的park()/unpark()方法。而使用synchronized关键字也可以阻塞线程,这是 JVM 提供的同步方式。但我们知道,Java 其实对线程 Thread 规定了一些状态,它在 Thread 类中的一个叫作 State 的内部枚举类里给出,一共有六个状态,其中一个状态就是BLOCKED
在这里插入图片描述
但是这个BLOCKED很有意思,它特指的是synchronized锁阻塞,而使用LockSupport 的park()方法来阻塞线程,线程不是进入BLOCKED,而是进入WAITING状态!
在这里插入图片描述
如上所示,扯得有点多了,我们回归正题。。。。

我们上面已经比较完整的分析完了acquire()的整个流程,现在总结一下,画个流程图
在这里插入图片描述
2. 独占式释放同步状态

当成功获取到同步状态(锁)的线程执行完毕后,就需要释放同步状态,使得后续节点能够获取同步状态。通过调用同步器的release()方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点。不多废话,直接看代码。

   public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;// 获取头节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);// 唤醒后继节点
            return true;
        }
        return false;
    }

release()是 AQS 提供的模板方法,它会调用自定义同步器重写的tryRelease()方法,不同同步器的实现各不相同,我们这里不对tryRelease()方法做过多的探讨。

我们来看一下unparkSuccessor()方法的源码。

    private void unparkSuccessor(Node node) {
        
        int ws = node.waitStatus;
        if (ws < 0) // 将当前线程的等待状态置为0
            compareAndSetWaitStatus(node, ws, 0);

        
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 注意这里的for循环,非常有意思,它是从尾部往前找的!
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        
        if (s != null)
            LockSupport.unpark(s.thread);// 唤醒线程
    }

因为持有同步状态(锁)的线程一定在头节点里,所以先检查的是头节点的后继节点,如果它的后继节点不为 null,且等待状态小于等于 0 那就直接唤醒后继节点。

看我的注释,for循环那里,很有趣,如果后继节点不满足要求,它并不是从头节点的后继节点开始从前往后遍历,而是从尾节点开始从后往前遍历,思考一下这是为什么?

对于 s == null 这种情况好理解,因为如果 s 为 null 的话,从前往后遍历会抛出 NullPointerException 异常。

那么对于 s.waitStatus > 0这种情况呢?从后往前遍历会不会破坏同步队列的 FIFO 特性 呢?绝对不会,仔细看源码你就会发现它一定会找到从头节点以后的第一个有效的节点。

 

二、共享模式

1. 共享式获取同步状态

共享式获取同步状态的顶层模板方法是acquireShared()方法,同acquire()方法一样,它也是对中断不敏感,并且 AQS 也提供了它的增强版本,那就是acquireSharedInterruptibly()/tryAcquireSharedNanos()这俩一个是响应中断的,另一个是可以超时退出的,同上文一样,这里只介绍最基础的acquireShared()方法,理解了acquireShared()方法的思想,理解它的增强版就会容易多了。

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

这里tryAcquireShared()依然需要自定义同步器去实现。但是 AQS 已经把其返回值的语义定义好了:

  1. 负数代表获取失败。
  2. 0 代表获取成功,但没有剩余资源。
  3. 正数表示获取成功,还有剩余资源,其他线程还可以去获取。

所以这里acquireShared()的流程就是:

  • tryAcquireShared()尝试获取资源,成功则直接返回。
  • 失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。

接下来,看一下doAcquireShared()方法。

    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; 
                        if (interrupted)
                            // 如果等待过程中中断过,这里需要中断,因为等待时不响应中断
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

具体看注释,发没发现其实跟acquireQueued()方法非常像。我们来看一下setHeadAndPropagate()方法.

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head;
        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();
        }
    }

代码比较简单,不写注释了,简单的总结一下,就是如果还有剩余资源,就会一直往后唤醒后继节点,Propagate 中文译过来就是传播的意思。

我们发现,其实acquireShared()方法的流程跟acquire()的流程非常相似,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这也就是共享的思想)。

2. 共享式释放同步状态

最后看一下,在共享模式下,如何释放同步状态,跟原来的思路一样,我们从 AQS 提供的顶层模板方法releaseShared()入手,一路往下走。

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();// 唤醒后继结点
            return true;
        }
        return false;
    }

至于tryReleaseShared()方法,老生常谈了,它由自定义同步器来实现,这里不多赘述。我们主要来看一下doReleaseShared()方法。

    private void doReleaseShared() {
        // 又是一个自旋CAS
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        // 如果CAS失败,就continue循环,自旋CAS
                        continue;            
                    unparkSuccessor(h);// 唤醒后继节点
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            if (h == head)                   
                break;
        }
    }

老套路,通过自旋 CAS 来释放同步状态,不多说了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值