CountDownLatch、CyclicBarrier和Semaphore

本文深入剖析了Java并发工具类CountDownLatch、CyclicBarrier和Semaphore的使用场景、源码及特性。CountDownLatch用于线程同步,等待所有线程执行完毕;CyclicBarrier允许一组线程等待所有线程到达屏障点后一起继续执行,具备重用性;Semaphore作为信号量,控制并发访问资源的数量。通过实例代码和源码分析,揭示了它们在多线程编程中的作用和实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

学习了aqs源码之后,我觉得很有必要把这三个类再系统的学习一下, 同时记录下学习的过程。如果没有看过aqs独占锁aqs共享锁的源码,我建议你先去学习aqs。aqs是基础,这三个类只是Doug Lea为了满足多线程编程下各种线程按照需要的场景来运行而写的三个工具类。aqs不懂,学习这三个类也只是走马观花,不得精髓。水平有限,文章中有误的地方也请不吝指正,互相帮助,共同学习。

CountDownLatch

CountDownLatch是我在项目中用的最多的一个并发类,现在我更喜欢用CompletableFuture(jdk1.8之后加入的)。
老规矩,先说CountDownLatch的用法和使用场景。
example:

public class CountDownLatchTest {
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(3);
        new Thread(()->{
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName());
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName()+ "解放了");
        }).start();
        new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName());
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName()+ "解放了");
        }).start();
        new Thread(()->{
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName());
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName()+ "解放了");
        }).start();
        System.out.println("等待所有线程执行完毕");
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("所有线程执行完毕");
    }
}

我们new了三个线程,用睡眠来模拟三个线程的执行时间,下面是执行结果。

等待所有线程执行完毕
Thread-1
Thread-1解放了
Thread-0
Thread-0解放了
Thread-2
Thread-2解放了
所有线程执行完毕

可以看到在使用了countDownLatch.await()方法之后,会等待上述三个线程执行完毕后才会去执行接下来的逻辑。我说一个使用场景,大家就明白了:
比如我们现在是在订单服务,要提供一个展示订单列表的接口,展示的字段里需要展示下单人的注册时间,需要展示商品的实时价格,这两个东西在订单服务里是查不到的,我们需要调用用户服务去查询用户的注册时间,调用商品服务去查询实时价格。如果之前代码是串行的,查订单列表要1秒,查注册时间要0.5秒,查实时价格要1秒,那么这个接口的总耗时大致是2.5秒。那么我们可以怎么优化呢,我们可以在查询到订单列表后,用两个线程分别去查注册时间和实时价格。等待两个线程都返回结果后,再去封装处理结果,这样接口的总耗时大致是2秒。我们使用CountDownLatch就优化了一个接口。当然,一个接口中如果有多个不相关的耗时sql也是可以这样优化的。

CountDownLatch源码

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

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

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        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;
            }
        }
    }
	public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
	public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
	// 调用此方法会释放一个资源
	public void countDown() {
        sync.releaseShared(1);
    }
	// 构造函数比较简单,就是传入一个资源数。
	public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

CountDownLatch的源码很少,主要就是这几个方法

  1. countDown()
    调用countDown()就释放一个资源,
  2. await()
    调用await()方法是调用aqs的acquireSharedInterruptibly()方法,这个方法和acquireShared()方法的区别就是这个是带中断处理的获取锁的方法。获取锁的条件就是Sync的return (getState() == 0) ? 1 : -1, 根据语义知道,当所有资源全部释放,state为0,此时调用await()的主线程就可以获取到锁。不为0就会等待。过程就不再赘述,aqs共享锁已经讲得很清楚了。源码大家可以点进去走一走,自己debug走一遍,啥都懂了。
  3. await(long timeout, TimeUnit unit)
    await()方法也可以传入超时参数,很好理解,等待一定的时间之后,没有完成就不再等待,继续执行后续逻辑。

CyclicBarrier

这个类我在项目中也没有用过,本着学习的原则,我把代码也跟了一遍,先放个例子:

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for (int i = 1; i < 4; i++) {
            new Worker(cyclicBarrier, i * 1000).start();
        }
    }

    static class Worker extends Thread {
        private CyclicBarrier barrier;
        private long time;

        public Worker(CyclicBarrier barrier, long time) {
            this.barrier = barrier;
            this.time = time;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "开始");
                Thread.sleep(time);
                System.out.println(Thread.currentThread().getName() + "执行完毕");
                barrier.await();
                System.out.println(Thread.currentThread().getName() + "解放了");
            } catch (Exception e) {
            }
        }
    }
}
Thread-0开始
Thread-2开始
Thread-1开始
Thread-0执行完毕
Thread-1执行完毕
Thread-2执行完毕
Thread-2解放了
Thread-0解放了
Thread-1解放了

这个例子中,我们循环new了三个线程,然后用了一个time参数模拟三个线程不同的执行耗时,我们可以看到,跟CountDownLatch不同的是,线程在执行完毕后并没有马上被解放,而是等所有线程都执行完毕,才解放自己。这说明CyclicBarrier是一组线程都执行完毕后,再一起解放自己,否则就一直等待。barrier译为栅栏,其实很形象的描述了这个功能,举个例子:长江上有个渡船,船夫规定每次只能乘坐三个人,且必须满三个人才出发去对岸,人不够就一直等待。
CyclicBarrier还有个特性就是可以重用,这点跟CountDownLatch不同,CountDownLatch不能重用。下面看一个重用的例子:

	for (int i = 1; i < 10; i++) {
            new Worker(cyclicBarrier, i * 1000).start();
    }

我们把模拟的任务数调成10,运行一下。

Thread-0开始
Thread-3开始
Thread-1开始
Thread-2开始
Thread-5开始
Thread-4开始
Thread-6开始
Thread-7开始
Thread-8开始
Thread-0执行完毕
Thread-1执行完毕
Thread-2执行完毕
Thread-2解放了
Thread-0解放了
Thread-1解放了
Thread-3执行完毕
Thread-4执行完毕
Thread-5执行完毕
Thread-5解放了
Thread-3解放了
Thread-4解放了
Thread-6执行完毕
Thread-7执行完毕
Thread-8执行完毕
Thread-8解放了
Thread-6解放了
Thread-7解放了

每个线程睡眠的时间不一样,但是可以看到,线程总是等到一组线程都执行完毕,也就是我们构造函数传的3个为一组, 才一起解放。看到这里,大家肯定会有疑问,这玩意有啥用啊。我们看一下CyclicBarrier的构造函数是有两个的。

	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参数表示多少个线程为一组。
barrierAction参数表示一组线程执行完毕后,最后一个执行的线程需要执行的任务。(这里为什么是最后一个线程执行任务,大家看了源码可以思考一下)

如果一组线程执行完毕,去执行一个特定的任务,是CyclicBarrier比较常用的场景。
example:

public class CyclicBarrierTest {
    static BlockingQueue deque = new ArrayBlockingQueue<Integer>(200);

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
            try {
                System.out.println("3人斗地主小队已匹配成功, " + "选手:" + deque.take() + "选手:" + deque.take() + "选手:" + deque.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        for (int i = 1; i < 12; i++) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
            new Worker(cyclicBarrier, i).start();
        }
    }

    static class Worker extends Thread {
        private CyclicBarrier barrier;
        private long no;

        public Worker(CyclicBarrier barrier, long no) {
            this.barrier = barrier;
            this.no = no;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "编号选手" + no + "匹配桌友");
                deque.add(no);
                barrier.await();
            } catch (Exception e) {
            }
        }
    }
}
Thread-0编号选手1匹配桌友
Thread-1编号选手2匹配桌友
Thread-2编号选手3匹配桌友
3人斗地主小队已匹配成功, 选手:1选手:2选手:3
Thread-3编号选手4匹配桌友
Thread-4编号选手5匹配桌友
Thread-5编号选手6匹配桌友
3人斗地主小队已匹配成功, 选手:4选手:5选手:6
Thread-6编号选手7匹配桌友
Thread-7编号选手8匹配桌友
Thread-8编号选手9匹配桌友
3人斗地主小队已匹配成功, 选手:7选手:8选手:9
Thread-9编号选手10匹配桌友
Thread-10编号选手11匹配桌友

我这里用一个大家常用的场景,斗地主小游戏匹配规则,每当一组三个选手进入匹配后,把这三个人从队列里拿出来开始游戏。如果不够三个人,比如10号和11号选手,就会无限等待12号选手。
看到这里,CyclicBarrier的用法大家想必很清楚了。

CyclicBarrier源码

// 先学习下CyclicBarrier的几个变量, Doug Lea用trip, parties, generation命名
	
	// 使用到了 ReentrantLock, java常用的锁的实现
	// The lock for guarding barrier entry 
    private final ReentrantLock lock = new ReentrantLock();
    // Condition to wait on until tripped 
    // 锁的Condition的用法
    private final Condition trip = lock.newCondition();
    // The number of parties 
    // 一组限制多少个资源
    private final int parties;
    // The command to run when tripped 
    private final Runnable barrierCommand;
    // The current generation 
    // 当前这一代,就是当前这一批次的资源
    private Generation generation = new Generation();
    /**
     * Number of parties still waiting. Counts down from parties to 0
     * on each generation.  It is reset to parties on each new
     * generation or when broken.
     */
    // 有多少资源在等待
    private int count;
	// Generation字面意思是一代的意思,broken代表这一代有没有出问题。
	private static class Generation {
        boolean broken = false;
    }

构造函数:

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

老规矩,从例子的调用入口开始。线程首先调用CyclicBarrier的await()方法。

	public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

await()方法也可以传入超时参数,最终都会调用dowait()方法。

	private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final Generation g = generation;
			// 首先判断这一代里有没有出事情,默认为false
            if (g.broken)
                throw new BrokenBarrierException();
			// 判断当前线程有没有发生中断, 注意这里调用的是interrupted()方法,如果线程被终端会清除中断标志位并返回true
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
			// count自检,为0的时候说明等待的线程凑够了一组。
            int index = --count;
            if (index == 0) {  // tripped
            	// 这是传入行为有没有被执行过的标志
                boolean ranAction = false;
                try {
                	// 执行传入行为
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // 开启下一代
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            // 没有凑够一组,既index不为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 {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }
				// 这里是被唤醒后的逻辑,判断broken标签,判断timed是否超时。
                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

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

首先这个方法是带有lock的,一次只能进来一个线程。代码是很好理解的,每次进来,count自减,减到0说明凑够了一组,然后执行barrier逻辑。如果count不为0,执行睡眠逻辑。注释写的已经很清楚了。barrier逻辑里,执行nextGeneration()方法:

	private void nextGeneration() {
		// 唤醒所有等待线程,然后重置count,重置generation
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }

我们来重新捋一下这个逻辑,每一个线程进来,count自减,不够一组就根据timed参数执行睡眠逻辑,并交出锁。其他线程继续竞争并持有锁进来。循环上述逻辑,当count为0时,凑够一组,执行barrier逻辑,执行barrierAction(就是构造函数传进来的行为),然后调用nextGeneration()唤醒其他线程,重置count,重置generation。继续这个循环。

这是正常的代码,我们来看一看异常的源码:

public class CyclicBarrierTest {
    static BlockingQueue deque = new ArrayBlockingQueue<Integer>(200);

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
            try {
                System.out.println("3人斗地主小队已匹配成功, " + "选手:" + deque.take() + "选手:" + deque.take() + "选手:" + deque.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        for (int i = 0; i < 8; i++) {
            try {
                Thread.sleep(1000);
                new Worker(cyclicBarrier, i).start();
            } catch (Exception e) {
            }
        }
    }

    static class Worker extends Thread {
        private CyclicBarrier barrier;
        private long no;// 选手编号

        public Worker(CyclicBarrier barrier, long no) {
            this.barrier = barrier;
            this.no = no;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "编号选手" + no + "匹配桌友");
                if (no == 5) {
                    Thread.currentThread().interrupt();
                }
                deque.add(no);
                barrier.await();
            } catch (InterruptedException e ) {
                System.out.println(Thread.currentThread().getName() + " 中断了");
            } catch (BrokenBarrierException e) {
                System.out.println(Thread.currentThread().getName() + " barrier is broken");
            }
        }
    }
}

我们在第五个选手匹配的时候,给线程一个中断,我们再看一下运行结果:

Thread-0编号选手0匹配桌友
Thread-1编号选手1匹配桌友
Thread-2编号选手2匹配桌友
3人斗地主小队已匹配成功, 选手:0选手:1选手:2
Thread-3编号选手3匹配桌友
Thread-4编号选手4匹配桌友
Thread-5编号选手5匹配桌友
Thread-5 中断了
Thread-3 barrier is broken
Thread-4 barrier is broken
Thread-6编号选手6匹配桌友
Thread-6 barrier is broken
Thread-7编号选手7匹配桌友
Thread-7 barrier is broken

我们可以看到, 选手5被中断,不仅和5一代的选手3和4,后面的选手都无法再被匹配。

	if (Thread.interrupted()) {
              breakBarrier();
              throw new InterruptedException();
    }
	private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

可以看到,中断后会调用breakBarrier()方法,而这个方法里把broken标签置为了true。后续线程进来后,会首先校验broken标签,然后抛出异常

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

我们可以看到, CyclicBarrier再把broken标签置为true之后,并不会自己再重置回false。既然可以重用,那么肯定不会被这一个小小的一次就终止整个barrier了,然后我翻了一下CyclicBarrier的全部方法。发现了reset()

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

reset方法里会同时调用breakBarrier()和nextGeneration(),我们可以在代码里catch代码块合理使用reset来保证CyclicBarrier继续重用
至此,线程中断的逻辑我们搞清楚了,我们再次捋一下。当一个线程发生中断,首先会置broken标志位为true, 然后唤醒当前在等待队列的所有线程,被唤醒的线程首先会验证broken标志位,然后抛出BrokenBarrierException。调用reset之后,后续线程依旧按照正常逻辑进行。

Semaphore

Semaphore其实比较简单,就是一种释放和获取资源数量都只能为1的一种aqs共享锁的实现。

public class SemaphoreTest {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 7; i++) {
            new Worker(semaphore, i).start();
        }
    }

    static class Worker extends Thread {
        private int no;
        private Semaphore semaphore;

        public Worker(Semaphore semaphore, int no) {
            this.no = no;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + "编号" + no + " 拿到了资源");
                Thread.sleep(no * 1000);
                System.out.println(Thread.currentThread().getName() + "编号" + no + " 释放了");
                semaphore.release();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

}
Thread-0编号0 拿到了资源
Thread-2编号2 拿到了资源
Thread-1编号1 拿到了资源
Thread-0编号0 释放了
Thread-6编号6 拿到了资源
Thread-1编号1 释放了
Thread-3编号3 拿到了资源
Thread-2编号2 释放了
Thread-4编号4 拿到了资源
Thread-3编号3 释放了
Thread-5编号5 拿到了资源
Thread-6编号6 释放了
Thread-4编号4 释放了
Thread-5编号5 释放了

可以看到,规定最大资源数,然后获取不到资源的等待资源的释放, 这里不再过多解释。不懂的去学习一下aqs的共享锁的实现

Semaphore源码分析

Semaphore的源码要注意的只有一点, Semaphore是有公平锁和非公平锁两种,看一下Semaphore的构造函数

	public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }
	static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;

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

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

主要对比一下tryAcquireShared的两种实现,公平锁会先判断是否存在等待队列,如果存在等待队列,则去排队等待。非公平锁会先获取资源,获取不到再去排队等待。进入了等待队列的线程,会严格遵循先来后到。也就是公平和非公平仅存在于最先竞争的那一刻。

Semaphore使用起来要注意一点,释放之前必须先获取资源。

	Semaphore semaphore = new Semaphore(5);
    semaphore.release();
    System.out.println(semaphore.availablePermits());
6

否则就会像上述这样,资源数大于规定数。

总结

  1. CountDownLatch不可以重用,CyclicBarrier可以。
  2. CountDownLatch线程在使用完毕,会立即释放资源,等待所有线程都执行完毕后,继续执行后续逻辑, CyclicBarrier线程使用完毕后,不会立即释放资源,而是等待所有线程都执行完毕后,最后一个执行完毕的线程执行既定的逻辑,再一起释放资源。
  3. Semaphore只是一种特殊的共享锁的实现,有公平和非公平两种方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值