JUC-并发工具(CountDownLatch\CycleBarrier\Semaphore)

一、简介

本文主要讲解并发编程中常用的三个工具,他们分别是CountDownLatch(闭锁)、CycleBarrier(循环栏栅)、Semaphore(信号量),三个工具都是在JUC并发包下提供的多线程开发工具,各自有各自的使用场景,在多线程开发中可以根据业务场景来选择合适的工具。三个工具是以AQS以及以AQS为基础的Lock来构成的,所以最底层还是AQS,关于AQS可以通过文章《JUC-AQS框架解析》来学习了解。

二、解析

2.1 CountDownLatch(闭锁)

使用场景?CountDownLatch也被称为闭锁,并发编程中的一个工具。当某一个线程执行到某一个位置时,需要等待其他线程完成后,再继续往下执行,这样的场景适合使用此工具。

使用方式?首先创建CountDownLatch的一个实例对象,指定等待锁数量k,一般等于剩余等待的线程数量,然后等待线程执行await()方法阻塞等待,其他线程执行countDown(),当k个countDown()执行后,则等待线程被唤醒,继续执行,简单的使用逻辑如下:

public class CountDownLatchTest {
    private static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "beginning do");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "end");
            countDownLatch.countDown();
        }).start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "beginning do");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "end");
            countDownLatch.countDown();
        }).start();

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

首先我们看下CountDownLatch的构造器方法,入参是一个int类型的大于等于0的count,在构造器内部创建了内部类Sync的一个实例,如下所示:

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

CountDownLatch的内部类Sync是继承了AQS《JUC-AQS框架解析》的一个子类,刚刚构造器传入的int类型的count参数,就是AQS中共享变量state的初始化数值,并且还可以看到Sync实现了AQS中的共享模式下的获取和释放共享资源的方法,也就是重写了tryAcquireShared()和tryReleaseShared()方法,源码如下:

   private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
        //初始化AQS的state为count
        Sync(int count) {
            setState(count);
        }
        //获取state
        int getCount() {
            return getState();
        }
        //共享模式下重写的获取资源方法
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
        //共享模式下重写的释放资源方法
        protected boolean tryReleaseShared(int releases) {
            //cas+自旋释放资源
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

 下面我们看下等待线程调用的await()方法,此方法功能是使调用线程暂时阻塞,直到countDown()方法释放完毕共享资源(state=0),才会唤醒等待线程,继续向下执行,过程如下:

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

可以看到CountDownLatch的await()方法调用的是AQS的acquireSharedInterruptibly()方法,AQS内此方法逻辑如下:

    //*************AQS**************//
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

 Sync内部重写了tryAcquireShared()方法,重写之后的逻辑是:(getState() == 0) ? 1 : -1;结果是大于0和小于0;

1.如果大于0,意味着共享资源释放完毕,整个方法返回,执行结束;

2.如果小于0,则意味着还有线程占用着共享资源没有释放,则进入执行AQS内的doAcquireSharedInterruptibly()方法,该方法和doAcquireShared()区别在于后者在过程中是不响应中断的,具体逻辑可以参考AQS《JUC-AQS框架解析》文章了解。如果countDown()方法没有执行count数量,则此时等待线程执行到此方法内部,会处于等待阻塞的状态。

下面我们看下其他线程的countDown()方法,具体逻辑如下:

    public void countDown() {
        sync.releaseShared(1);
    }

 直接调用的是Sync的父类AQS的releaseShared()方法,逻辑如下:

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

 首先调用自定义同步器重写的tryReleaseShared(),也就是Sync重写的方法逻辑,如下逻辑:

        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    

可以看到逻辑是cas+自旋的方式,设置state值减一,然后返回state是否等于0,如果等于0,则进入doReleaseShared()逻辑,则去唤醒队列节点去获取锁等操作;如果不等于0,则不去唤醒队列节点。

总结过程:以上大概就是CountDownLatch工具的执行过程,我们总结过程如下:

1.创建CountDownLatch的实例对象,设定入参count值,初始化AQS对的state,主线程也就是等待线程去执行await()方法;

2.主线程执行到await()方法时,调用AQS的acquireSharedInterruptibly()方法,如果此时state!=0,则阻塞此方法;

3.如果此时state=0,则等待线程继续执行,直到完毕;

3.其他线程执行countDown()方法时,使用cas+自旋的方式,设置state值减一,如果state=0,则唤醒队列的后续节点;

4.如果state!=0,则执行完毕;

注意!CountDownLatch工具在AQS构建的队列,只会有两个节点,一个是空的头结点(AQS初始化),另一个就是阻塞的等待线程节点。

2.2 CycleBarrier(循环栏栅)

功能?CycleBarrier与CountDownLatch有着类似的作用,但是二者区别又很明显,CycleBarrier是可以循环使用的,且当全部线程都到达某一个Barrier屏障后,然后全部继续向下执行。

简单的使用方式如下:

public class CyclicBarrierTest {
    private static CyclicBarrier cycleBarrier = new CyclicBarrier(2, () -> System.out.println("-last task over-"));

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " beginning do");
            try {
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + " sleep end");
                cycleBarrier.await();
                System.out.println(Thread.currentThread().getName() + " do over");
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " beginning do");
            try {
                cycleBarrier.await();
                System.out.println(Thread.currentThread().getName() + " do over");
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();
        cycleBarrier.reset();
    }
}

首先看一下他的主要成员变量,包括锁、数量控制以及锁的条件:

    //执行的await()方法的加锁工具,避免并发造成count不准
    private final ReentrantLock lock = new ReentrantLock();
    //该锁的条件,用于阻塞线程知道满足条件
    private final Condition trip = lock.newCondition();
    //构造函数传入参数,指定参与的线程数量
    private final int parties;
    //构造参数传入,用于默认的执行任务
    private final Runnable barrierCommand;
    //作为代标识
    private Generation generation = new Generation();
    //已经执行await()的数量,初始化值为parties
    private int count;

下面我们看下CycleBarrier的执行过程,首先我们看下他的创建入口:

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

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

可以看到实际上是调用的两个参数的构造方法,入口有两个参数,一个是parties,代表着必须执行await()方法的线程数量,才可以到达栅栏继续往下执行,另外一个是Runnable类型的barrierAction,当parties数量的线程执行await()方法之后,则运行此配置的任务。

下面我看下核心方法await(),方法逻辑如下:

    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

 还有一个await()方法支持传入等待时间的,底层都是同样调用doAwait()方法,下面我看下此方法:

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁
        try {
            final Generation g = generation;

            //如果这一代已经结束,则抛出异常
            if (g.broken)
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            //执行count减一
            int index = --count;
            if (index == 0) {  //如果全部执行完毕,count=0,则唤醒所有线程,执行入参的任务
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    nextGeneration();//开始下一代
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            //如果count!=0,则循环开始,直到唤醒、本代结束(broken=0)、中断、或者超时
            for (;;) {
                try {
                    if (!timed)
                        trip.await();//进入等待状态,并释放锁
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

  首先我们看到进入方法先进行lock加锁操作,后续进行count减一操作,如果count=0,则唤醒所有线程执行后续逻辑,如果count!=0则使线程进入等待状态,具体逻辑如下:

1.lock加锁,判断broken是否为true,如果是则抛出异常;继续判断线程是否中断,如果中断则抛出异常;

2.lock内执行--count,然后判断count是否为0;

3.如果count=0,表示parties数量的线程执行await()方法完毕,则执行默认任务,唤醒其他线程,归置count和generation;

4.如果count!=0,则表示还有线程没有执行await()方法,则走入后边的自旋,按照传入的时间参数进入等待状态(等待时间依据配置);

5.如果没有配置等待时间,则会持续等待直到被唤醒;如果配置了等待时间,则在等待配置时间内没有被唤醒,则抛出timeout异常,结束;

刚刚上面流程提到,如果count=0则进入nextGeneration()方法,过程如下:

    private void nextGeneration() {
        //唤醒所有线程
        trip.signalAll();
        //重置count和generation(可重用)
        count = parties;
        generation = new Generation();
    }

 唤醒所有等待线程继续执行,初始化count为parties值,新建一个generation对象,表示新的一代开始。

当发生异常情况时候,会执行breakBarrier()方法,过程如下:

    private void breakBarrier() {
        //本代标识中断标识true
        generation.broken = true;
        //重置count
        count = parties;
        //通知所有线程(broken=true都会抛出异常)
        trip.signalAll();
    }

可以看到broken中断标识的值变成true,初始化count,通知所有线程,此时其他线程被唤醒后,执行至broken的判断,都会抛出BrokenBarrierException的异常,从而停止。

总结过程!CyclicBarrier的核心基本都在doAwait()方法中,所以理解上面分析的此方法的逻辑,便可以理解此工具的工作逻辑。

2.3 Semaphore(信号量)

信号量Semaphore可以控制同时执行代码体的线程数量,比如说,在Semaphore控制代码间,只能有k个线程同时执行,类似于lock加锁间只允许一个线程去执行,而Semaphore是可控的线程数量,简单的使用方式如下:

public class SemaphoreTest {
    private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    private static Semaphore semaphore = new Semaphore(2);

    public static void main(String[] args) {
        SemaphoreTest semaphoreTest = new SemaphoreTest();
        for (int i = 0; i < 10; i++) {
            new Thread(semaphoreTest::doThing).start();
        }
    }

    private void doThing() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + "-start : " + getFormatTimeStr());
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "-end : " + getFormatTimeStr());
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static String getFormatTimeStr() {
        return sf.format(new Date());
    }
}

执行结果参考如下:

可以大致的看出同时执行的线程是两个,当一个线程结束的时候,另一个线程可以开始,始终维持这最多两个线程执行这段期间的代码逻辑,下面我们分析一下它的执行逻辑。

构造函数!首先我们看下他的程序入口:

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

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

Semaphore分为公平和非公平两种方式,构造函数有两个入参,其中一个是int类型的permits参数,也就是可以并行的线程数量,另外是boolean类型的fair参数,可以指定是否是公平模式的,默认是非公平方式的。

Sync是Semaphore内部类,他继承自AQS,可以看到我们传进去的permits,作为了AQS中的state的初始值。

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

获取资源!下面我们以默认的非公平的方式分析,首先我们看下acquire()方法,逻辑如下:

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

Sync是Semaphore的一个内部类,类似于我们上边讲解的CountDownLatch工具那样,使用内部类的形式继承AQS来实现响应功能,acquireSharedInterruptibly()方法是AQS内的共享模式的顶层入口,逻辑如下(类似于CountDownLatch工具):

    //*************AQS**************//
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

在看Semaphore重写的tryAcquireShared()方法逻辑:

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }

可以看到调用的是父类Sync的nonfairTryAcquireShared()方法,逻辑如下:

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

总体思想是自旋+cas,获取state值,然后减去本次将要占用的资源数量,判断剩余值remaining。如果remaining<0,则证明本次无法获取足够资源,此时返回负数,则进入刚刚上面的判断,进入AQS的doAcquireSharedInterruptibly()方法,线程进入等待状态,等待唤醒执行。如果remaining>=0,则证明有足够资源,则进行cas设置,直到成功,整个方法返回。

获取锁的方式在于自定义同步器的tryAcquireShared()方法的重写逻辑,作为对比,我们看下公平模式下获取锁的实现:

        protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())//判断是否是最靠前节点(公平)
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

不同点在于公平模式下多了一个执行步骤,也就是hasQueuedPredecessors()方法的执行逻辑,前面文章中提到了此方法是用于判断本线程节点是否有资格获取锁,包括空队列、当前线程刚好是头节点的下一个节点等情况下才有资格去获取共享资源。

释放资源!下面我们看下释放资源的方法release(),逻辑如下:
 

    public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }

接着我们看AQS的releaseShared()方法:

    //*************AQS**************//
    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)
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

逻辑很简单,自旋+cas方式释放共享资源,其中会校验一下共享资源是否释放溢出。当释放资源成功后,则执行doReleaseShared()方法,唤醒后续节点去尝试获取锁。

整体过程!Semphore整体的工作流程:

1.首先通过构造函数创建Semaphore实例,指定并发数量以及按照需要指定默认的执行任务;

2.线程开始执行,调用Semaphore实例的acquire()方法,底层调用AQS共享模式下的获取锁的方式,重写获取锁的方法。cas+自旋去判断当前state值是否小于0;

3.如果state减去此次占用的资源后,是小于0的,则表示本线程需要去队列中等待;

4.如果state大于等于0,则cas更新state的值,如果成功则返回,如果失败则下一次循环;

注意Semaphore是区分公平模式和非公平模式的,默认是以非公平方式进行的。

四、资源地址

官网:http://www.java.com

文档:《Thinking in java》

jdk1.8版本源码

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值