AQS源码剖析第三篇--共享模式


系列文章:

AQS源码剖析第一篇—全貌概览

AQS源码剖析第二篇–公平与非公平,条件队列和线程中断


这篇,我们的关注点是 AQS 最后的部分,AQS 共享模式的使用。

本文先用 CountDownLatch 将共享模式说清楚,然后顺着把其他 AQS 相关的类 CyclicBarrier、Semaphore 的源码一起过一下。


CountDownLatch

CountDownLatch 这个类是比较典型的 AQS 的共享模式的使用,这是一个高频使用的类。latch 的中文意思是门栓、栅栏,具体怎么解释我就不废话了,大家随意,看两个例子就知道在哪里用、怎么用了。

假设我们有3个任务,那么我们会用 3 来初始化一个 CountDownLatch,然后将这个 latch 的引用传递到各个线程中,在每个线程完成了任务后,调用 latch.countDown() 代表完成了一个任务。

调用 latch.await() 的方法的线程会阻塞,直到所有的任务完成。

package com.aqs;

import java.util.concurrent.CountDownLatch;

/**
 * @author 大忽悠
 * @create 2022/10/2 16:56
 */
public class AqsShare {
    /**
     * 开始信号
     */
    private static final CountDownLatch START_SIGNAL=new CountDownLatch(1);
    /**
     * 结束信号
     */
    private static final CountDownLatch END_SIGNAL=new CountDownLatch(3);


    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; ++i) {
            new Thread(new Worker(START_SIGNAL, END_SIGNAL)).start();
        }

        // 这边插入一些代码,确保上面的每个线程先启动起来,才执行下面的代码。
        doSomethingElse();
        // 因为这里 N == 1,所以,只要调用一次,那么所有的 await 方法都可以通过
        START_SIGNAL.countDown();
        doSomethingElse();
        // 等待所有任务结束
        END_SIGNAL.await();
        System.out.println("所有任务执行结束");
    }

    private static void doSomethingElse() {
        try {
            Thread.sleep(1000L);
            System.out.println("Main线程休息好了...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static class Worker implements Runnable{
        /**
         * 开始信号
         */
        private final CountDownLatch startSignal;
        /**
         * 结束信号
         */
        private final CountDownLatch endSignal;

        public Worker(CountDownLatch startSignal, CountDownLatch endSignal) {
            this.startSignal = startSignal;
            this.endSignal = endSignal;
        }


        @Override
        public void run() {
            try {
                // 为了让所有线程同时开始任务,我们让所有线程先阻塞在这里
                // 等大家都准备好了,再打开这个门栓
                startSignal.await();
                doWork();
                endSignal.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doWork() throws InterruptedException {
              System.out.printf("当前线程[%s],努力工作中...\r\n",Thread.currentThread().getName());
              Thread.sleep(3000L);
            System.out.printf("当前线程[%s],工作结束\r\n",Thread.currentThread().getName());
        }
    }
}

在这里插入图片描述

3个新开启的线程都调用了startSignal.await() 进行阻塞等待,它们阻塞在栅栏上,只有当条件满足的时候(startSignal.countDown()),它们才能同时通过这个栅栏,目的是让所有的线程站在一个起跑线上。
在这里插入图片描述
如果始终只有一个线程调用 await 方法等待任务完成,那么 CountDownLatch 就会简单很多,所以之后的源码分析读者一定要在脑海中构建出这么一个场景:有 m 个线程是做任务的,有 n 个线程在某个栅栏上等待这 m 个线程做完任务,直到所有 m 个任务完成后,n 个线程同时通过栅栏。

CountDownLatch 非常实用,我们常常会将一个比较大的任务进行拆分,然后开启多个线程来执行,等所有线程都执行完了以后,再往下执行其他操作。


源码分析

构造方法,需要传入一个不小于 0 的整数:

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

CountDownLatch内部提供的同步器实现如下:

private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

       //count计数为0,任务都完成了,就返回1,否则返回-1
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        //count计数自旋减一,如果减去一后为0,则返回true,否则返回false  
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

代码都是套路,先分析套路:AQS 里面的 state 是一个整数值,这边用一个 int count 参数其实初始化就是设置了这个值,所有调用了 await 方法的等待线程会挂起,然后有其他一些线程会做 state = state - 1 操作,当 state 减到 0 的同时,那个将 state 减为 0 的线程会负责唤醒 所有调用了 await 方法的线程。


对于 CountDownLatch,我们仅仅需要关心两个方法,一个是 countDown() 方法,另一个是 await() 方法。

countDown() 方法每次调用都会将 state 减 1,直到 state 的值为 0;而 await 是一个阻塞方法,当 state 减为 0 的时候,await 方法才会返回。await 可以被多个线程调用,读者这个时候脑子里要有个图:所有调用了 await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。


await源码分析

首先,我们来看 await() 方法,它代表线程阻塞,等待 state 的值减为 0。

public void await() throws InterruptedException {
    //共享模式下响应中断的获取资源获取
    sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //如果获取资源过程中发生了中断,会给予响应
    if (Thread.interrupted())
        throw new InterruptedException();

    //尝试在共享模式下获取资源
    if (tryAcquireShared(arg) < 0)
    //资源未能获取成功,则准备进入阻塞队列
        doAcquireSharedInterruptibly(arg);
}

//尝试在共享模式下去获取资源
//对于CountDownLatch来说,资源是否获取成功,等价于count是否为0
// 只有当 state == 0 的时候,这个方法才会返回 1,表示资源获取成功,那就不需要将当前线程弄到阻塞队列等着了
//如果资源获取失败,则返回-1,下一步就是让当前线程去阻塞队列等着了
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

doAcquireSharedInterruptibly 从方法名我们就可以看出,这个方法是获取共享锁,并且此方法是可中断的(中断的时候抛出 InterruptedException 退出这个方法)。

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //将当前线程以共享模式加入阻塞队列
        final Node node = addWaiter(Node.SHARED);
        try {
           //死轮询直到获取到共享锁
            for (;;) {
                //只有前驱节点是head哑结点的情况下,才有机会去抢共享锁
                final Node p = node.predecessor();
                if (p == head) {
                    //抢锁成功对于CountDownLatch意味着count=0
                    int r = tryAcquireShared(arg);
                    //返回值大于0,表示count=0了,那么需要链式唤醒后续其他阻塞线程
                    if (r >= 0) {
                        //设置当前节点为头结点,并且唤醒阻塞队列上模式为shared的后续节点 
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        return;
                    }
                }
                //抢锁失败就挂起,和之前一样---这里抢锁失败意味着count!=0
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

如果当前阻塞队列中第一个节点绑定的线程阻塞被唤醒后,发现此时count=0了,那么会去链式唤醒在阻塞队列上的后续模式为shared的节点。
在这里插入图片描述

链式唤醒的逻辑存在于setHeadAndPropagate中:

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head;
        //设置当前节点为新的头结点
        setHead(node);
        //如果当前被唤醒线程的node节点的下一个节点是共享模式,那么会通过doReleaseShared唤醒后继节点
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //链式唤醒的核心,后面分析doReleaseShared方法的时候就会明白
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

下面再通过流程图的方式把上面的流程过一遍:

  • 线程 t3 经过第 1 步 addWaiter 入队以后,我们应该可以得到这个:

在这里插入图片描述
由于 tryAcquireShared 这个方法会返回 -1,所以 if (r >= 0) 这个分支不会进去。到 shouldParkAfterFailedAcquire 的时候,t3 将 head 的 waitStatus 值设置为 -1,如下:

在这里插入图片描述
然后进入到 parkAndCheckInterrupt 的时候,t3 挂起。

我们再分析 t4 入队,t4 会将前驱节点 t3 所在节点的 waitStatus 设置为 -1,t4 入队后,应该是这样的:

在这里插入图片描述
然后,t4 也挂起。接下来,t3 和 t4 就等待唤醒了。

接下来,我们来看唤醒的流程。为了让下面的示意图更丰富些,我们假设用 10 初始化 CountDownLatch。

在这里插入图片描述


countDown源码分析

    public void countDown() {
        sync.releaseShared(1);
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

releaseShared是由AQS框架提供的模板方法,其中tryReleaseShared如何释放共享锁的逻辑应该由子类实现,因此我们来看看countDownLatch中是如何实现的吧。

        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

这个方法很简单,用自旋的方法实现 state 减 1,如果减去一后,state为0,返回true。

state为0,对于CountDownLatch来说,即count为0,即计数为0了,那么就需要唤醒所有因为await阻塞的线程了。


doReleaseShared是由AQS框架提供的,共享模式下唤醒逻辑:

// 调用这个方法的时候,state == 0
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        //阻塞队列不为空
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //说明头结点后面存在节点需要被唤醒
            if (ws == Node.SIGNAL) {
                // 将 head 的 waitStatue 设置为 0---因为唤醒头结点的后继节点后,后继节点后将自己作为新的头结点
                //旧的头结点会从阻塞队列中移除
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 唤醒 head 的后继节点,也就是阻塞队列中靠前面满足条件的节点
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // todo
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

这段代码先不看完,就看到unparkSuccessor(h),然后结束。

当unparkSuccessor(h)唤醒头节点的第一个后继节点后,因为此时count=0,所以:

在这里插入图片描述
setHeadAndPropagate开始了链式唤醒:
在这里插入图片描述


唤醒第一个后继节点后,还存在下面几种可能情况:

在这里插入图片描述

  1. 被唤醒的后继节点所在线程将自己设置为新的头结点时,调用CountDown的线程已经执行完了,到了 if (h == head)判断逻辑
// 调用这个方法的时候,state == 0
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        //阻塞队列不为空
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //说明头结点后面存在节点需要被唤醒
            if (ws == Node.SIGNAL) {
                // 将 head 的 waitStatue 设置为 0---因为唤醒头结点的后继节点后,后继节点后将自己作为新的头结点
                //旧的头结点会从阻塞队列中移除
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
            // 唤醒 head 的后继节点,也就是阻塞队列中靠前面满足条件的节点
                unparkSuccessor(h);`在这里插入代码片`
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // todo
                continue;                // loop on failed CAS
        }
        //此时头结点还没有被后继节点重新设置,因此满足条件,跳出循环
        if (h == head)                   // loop if head changed
            break;
    }
}
  1. 被唤醒的后继节点所在线程将自己设置为新的头结点时,调用CountDown的线程还没执行完 if (h == head)判断逻辑
// 调用这个方法的时候,state == 0
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        //阻塞队列不为空
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //说明头结点后面存在节点需要被唤醒
            if (ws == Node.SIGNAL) {
                // 将 head 的 waitStatue 设置为 0---因为唤醒头结点的后继节点后,后继节点后将自己作为新的头结点
                //旧的头结点会从阻塞队列中移除
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
         // 唤醒 head 的后继节点,也就是阻塞队列中靠前面满足条件的节点
                unparkSuccessor(h);
            }
            //当唤醒到阻塞队列中最后一个节点时,自然此时ws为0了
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // todo
                continue;                // loop on failed CAS
        }
        //此时头结已经被后继节点重新设置,因此不满足条件,继续循环
        if (h == head)                   // loop if head changed
            break;
    }
}

CyclicBarrier

字面意思是“可重复使用的栅栏”或“周期性的栅栏”,总之不是用了一次就没用了的,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。看如下示意图,CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。

在这里插入图片描述
首先,CyclicBarrier 的源码实现和 CountDownLatch 大相径庭,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现。

因为 CyclicBarrier 的源码相对来说简单许多,读者只要熟悉了前面关于 Condition 的分析,那么这里的源码是毫无压力的,就是几个特殊概念罢了。

先用一张图来描绘下 CyclicBarrier 里面的一些概念,和它的基本使用流程:

在这里插入图片描述
看图我们也知道了,CyclicBarrier 的源码最重要的就是 await() 方法了。

简单使用演示如下:

package com.aqs;

import lombok.SneakyThrows;


import java.util.concurrent.*;

/**
 * @author 大忽悠
 * @create 2022/10/2 16:56
 */
public class AqsShare {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(10, () -> {
            System.out.println("所有线程已经到达栅栏处,准备放行");
        });

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(()->{
                try {
                    while(true){
                        System.out.println("马上到达栅栏处...");
                        sleepRandom();
                        cyclicBarrier.await();
                        System.out.println("栅栏放开...");
                    }
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
    }

    @SneakyThrows
    private static void sleepRandom() {
        int sleepTime = ThreadLocalRandom.current().nextInt(1000, 5000);
        Thread.sleep(sleepTime);
    }
}

源码分析

public class CyclicBarrier {
    // 我们说了,CyclicBarrier 是可以重复使用的,我们把每次从开始使用到穿过栅栏当做"一代",或者"一个周期"
    private static class Generation {
        boolean broken = false;
    }

    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();

    // CyclicBarrier 是基于 Condition 的
    // Condition 是“条件”的意思,CyclicBarrier 的等待线程通过 barrier 的“条件”是大家都到了栅栏上
    private final Condition trip = lock.newCondition();

    // 参与的线程数
    private final int parties;

    // 如果设置了这个,代表越过栅栏之前,要执行相应的操作
    private final Runnable barrierCommand;

    // 当前所处的“代”
    private Generation generation = new Generation();

    // 还没有到栅栏的线程数,这个值初始为 parties,然后递减
    // 当count为0时,说明到了栅栏被放开的时刻
    private int count;

    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

    public CyclicBarrier(int parties) {
        this(parties, null);
    }

首先,先看怎么开启新的一代:

// 开启新的一代,当最后一个线程到达栅栏上的时候,调用这个方法来唤醒其他线程,同时初始化“下一代”
private void nextGeneration() {
    // 首先,需要唤醒所有的在栅栏上等待的线程
    trip.signalAll();
    // 更新 count 的值
    count = parties;
    // 重新生成“新一代”
    generation = new Generation();
}

看看怎么打破一个栅栏:

private void breakBarrier() {
    // 设置状态 broken 为 true
    generation.broken = true;
    // 重置 count 为初始值 parties
    count = parties;
    // 唤醒所有已经在等待的线程
    trip.signalAll();
}

这两个方法之后用得到,现在开始分析最重要的等待通过栅栏方法 await 方法:

// 不带超时机制
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
// 带超时机制,如果超时抛出 TimeoutException 异常
public int await(long timeout, TimeUnit unit)
    throws InterruptedException,
           BrokenBarrierException,
           TimeoutException {
    return dowait(true, unit.toNanos(timeout));
}

继续往里看:

private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
    final ReentrantLock lock = this.lock;
    // 先要获取到锁,然后在 finally 中要记得释放锁
    // 如果记得 Condition 部分的话,我们知道 condition 的 await() 会释放锁,被 signal() 唤醒的时候需要重新获取锁
    lock.lock();
    try {
        final Generation g = generation;
        // 检查栅栏是否被打破,如果被打破,抛出 BrokenBarrierException 异常
        if (g.broken)
            throw new BrokenBarrierException();
        // 检查中断状态,如果中断了,抛出 InterruptedException 异常
        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }
        // index 是这个 await 方法的返回值
        // 注意到这里,这个是从 count 递减后得到的值
        int index = --count;

        // 如果等于 0,说明所有的线程都到栅栏上了,准备通过
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                // 如果在初始化的时候,指定了通过栅栏前需要执行的操作,在这里会得到执行
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                // 如果 ranAction 为 true,说明执行 command.run() 的时候,没有发生异常退出的情况
                ranAction = true;
                // 唤醒等待的线程,然后开启新的一代
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    // 进到这里,说明执行指定操作的时候,发生了异常,那么需要打破栅栏
                    // 之前我们说了,打破栅栏意味着唤醒所有等待的线程,设置 broken 为 true,重置 count 为 parties
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        // 如果是最后一个线程调用 await,那么上面就返回了
        // 下面的操作是给那些不是最后一个到达栅栏的线程执行的
        for (;;) {
            try {
                // 如果带有超时机制,调用带超时的 Condition 的 await 方法等待,直到最后一个线程调用 await
                if (!timed)
                    trip.await();
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                // 如果到这里,说明等待的线程在 await(是 Condition 的 await)的时候被中断
                if (g == generation && ! g.broken) {
                    // 打破栅栏
                    breakBarrier();
                    // 打破栅栏后,重新抛出这个 InterruptedException 异常给外层调用的方法
                    throw ie;
                } else {
                    // 到这里,说明 g != generation, 说明新的一代已经产生,即最后一个线程 await 执行完成,
                    // 那么此时没有必要再抛出 InterruptedException 异常,记录下来这个中断信息即可
                    // 或者是栅栏已经被打破了,那么也不应该抛出 InterruptedException 异常,
                    // 而是之后抛出 BrokenBarrierException 异常
                    Thread.currentThread().interrupt();
                }
            }

              // 唤醒后,检查栅栏是否是“破的”
            if (g.broken)
                throw new BrokenBarrierException();

            // 这个 for 循环除了异常,就是要从这里退出了
            // 我们要清楚,最后一个线程在执行完指定任务(如果有的话),会调用 nextGeneration 来开启一个新的代
            // 然后释放掉锁,其他线程从 Condition 的 await 方法中得到锁并返回,然后到这里的时候,其实就会满足 g != generation 的
            // 那什么时候不满足呢?barrierCommand 执行过程中抛出了异常,那么会执行打破栅栏操作,
            // 设置 broken 为true,然后唤醒这些线程。这些线程会从上面的 if (g.broken) 这个分支抛 BrokenBarrierException 异常返回
            // 当然,还有最后一种可能,那就是 await 超时,此种情况不会从上面的 if 分支异常返回,也不会从这里返回,会执行后面的代码
            if (g != generation)
                return index;

            // 如果醒来发现超时了,打破栅栏,抛出异常
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

好了,我想我应该讲清楚了吧,我好像几乎没有漏掉任何一行代码吧?

下面开始收尾工作。

首先,我们看看怎么得到有多少个线程到了栅栏上,处于等待状态:

public int getNumberWaiting() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return parties - count;
    } finally {
        lock.unlock();
    }
}

判断一个栅栏是否被打破了,这个很简单,直接看 broken 的值即可:

public boolean isBroken() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return generation.broken;
    } finally {
        lock.unlock();
    }
}

前面我们在说 await 的时候也几乎说清楚了,什么时候栅栏会被打破,总结如下:

  • 中断,我们说了,如果某个等待的线程发生了中断,那么会打破栅栏,同时抛出 InterruptedException 异常;
  • 超时,打破栅栏,同时抛出 TimeoutException 异常;
  • 指定执行的操作抛出了异常,这个我们前面也说过。

最后,我们来看看怎么重置一个栅栏:

public void reset() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        breakBarrier();   // break the current generation
        nextGeneration(); // start a new generation
    } finally {
        lock.unlock();
    }
}

我们设想一下,如果初始化时,指定了线程 parties = 4,前面有 3 个线程调用了 await 等待,在第 4 个线程调用 await 之前,我们调用 reset 方法,那么会发生什么?

首先,打破栅栏,那意味着所有等待的线程(3个等待的线程)会唤醒,await 方法会通过抛出 BrokenBarrierException 异常返回。然后开启新的一代,重置了 count 和 generation,相当于一切归零了。

怎么样,CyclicBarrier 源码很简单吧。


Semaphore

有了 CountDownLatch 的基础后,分析 Semaphore 会简单很多。Semaphore 是什么呢?它类似一个资源池(读者可以类比线程池),每个线程需要调用 acquire() 方法获取资源,然后才能执行,执行完后,需要 release 资源,让给其他的线程用。

大概大家也可以猜到,Semaphore 其实也是 AQS 中共享锁的使用,因为每个线程共享一个池嘛。

套路解读:创建 Semaphore 实例的时候,需要一个参数 permits,这个基本上可以确定是设置给 AQS 的 state 的,然后每个线程调用 acquire 的时候,执行 state = state - 1,release 的时候执行 state = state + 1,当然,acquire 的时候,如果 state = 0,说明没有资源了,需要等待其他线程 release。


构造方法:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

这里和 ReentrantLock 类似,用了公平策略和非公平策略。

看 acquire 方法:

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
public void acquireUninterruptibly() {
    sync.acquireShared(1);
}
public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}
public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
}

这几个方法也是老套路了,大家基本都懂了吧,这边多了两个可以传参的 acquire 方法,不过大家也都懂的吧,如果我们需要一次获取超过一个的资源,会用得着这个的。

我们接下来看不抛出 InterruptedException 异常的 acquireUninterruptibly() 方法吧:

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

前面说了,Semaphore 分公平策略和非公平策略,我们对比一下两个 tryAcquireShared 方法:

// 公平策略:
protected int tryAcquireShared(int acquires) {
    for (;;) {
        // 区别就在于是不是会先判断是否有线程在排队,然后才进行 CAS 减操作
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}
// 非公平策略:
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

也是老套路了,所以从源码分析角度的话,我们其实不太需要关心是不是公平策略还是非公平策略,它们的区别往往就那么一两行。

我们再回到 acquireShared 方法,

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

由于 tryAcquireShared(arg) 返回小于 0 的时候,说明 state 已经小于 0 了(没资源了),此时 acquire 不能立马拿到资源,需要进入到阻塞队列等待,虽然贴了很多代码,不在乎多这点了:

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
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //资源不足就挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这个方法我就不介绍了,线程挂起后等待有资源被 release 出来。接下来,我们就要看 release 的方法了:

// 任务介绍,释放一个资源
public void release() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        // 溢出,当然,我们一般也不会用这么大的数
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

tryReleaseShared 方法总是会返回 true,然后是 doReleaseShared,这个也是我们熟悉的方法了,我就贴下代码,不分析了,这个方法用于唤醒所有的等待线程:

//唤醒后继节点,由后继节点在资源剩余情况下,继续链式唤醒后续节点
private void doReleaseShared() {
    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;
    }
}

Semphore 的源码确实很简单,基本上都是分析过的老代码的组合使用了。


参考

一行一行源码分析清楚 AbstractQueuedSynchronizer (三)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值