CountDownLatch和CyclicBarrier

本文深入剖析了CountDownLatch与CyclicBarrier两种并发控制工具的区别与应用场景。CountDownLatch用于等待一个事件发生,而CyclicBarrier则适用于一组线程相互等待,直至所有线程到达屏障点才继续执行。文章详细解释了两者的实现原理、核心方法及使用案例。

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

一、CountDownLatch

CountDownLatch是拦截线程等待事件发生,提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。

1、下面是java源码的两个demo:

(1)

	/**
	 * 
	 * startSignal:司机准备好之前,不允许工人上车;
	 * doneSignal:司机需要等待所有工人完成工作
	 */
	class Driver {

		void main() throws InterruptedException {
			CountDownLatch startSignal = new CountDownLatch(1);// 开始信号
			CountDownLatch doneSignal = new CountDownLatch(10);// 结束信号

			for (int i = 0; i < 10; ++i) {
				Thread d = new Thread(new Worker(startSignal, doneSignal));
				d.setName("工人"+i);
				d.start();
			}
			System.out.println("检修、发动大汽车");
			startSignal.countDown(); //司机准备
			System.out.println("等待工人..");
			doneSignal.await(); //等待工人
			System.out.println("出发了...");
		}
	}

	class Worker implements Runnable {
		private final CountDownLatch startSignal;
		private final CountDownLatch doneSignal;
		
		Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
			this.startSignal = startSignal;
			this.doneSignal = doneSignal;
		}

		public void run() {
			try {
				startSignal.await();//等待司机准备完成
				doWork();
				doneSignal.countDown();
			} catch (InterruptedException ex) {
			} 
		}
		
		void doWork() {
			System.out.println(Thread.currentThread().getName()+"工作");
		}
	}

(2)

/**
	 * 
	 *另一种常用方式是将一个问题分解成多个部分,每个部分用一个线程表示,并且执行这个部分进行countdown操作,将这些部分都提交到executor执行
	 *
	 *
	 */
	class Driver2 {
		private static final int N = 5;

		void main() throws InterruptedException {
			CountDownLatch doneSignal = new CountDownLatch(N);
			Executor e = Executors.newFixedThreadPool(N);

			for (int i = 0; i < N; ++i) // create and start threads
				e.execute(new WorkerRunnable(doneSignal, i));
			doneSignal.await(); // wait for all to finish
			System.out.println("======完成======");
		}
	}

	class WorkerRunnable implements Runnable {
		private final CountDownLatch doneSignal;
		private final int i;

		WorkerRunnable(CountDownLatch doneSignal, int i) {
			this.doneSignal = doneSignal;
			this.i = i;
		}

		public void run() {
			doWork(i);
			doneSignal.countDown();
		}

		void doWork(int i) {
			System.out.println("执行第"+i+"部分");
		}
	}

 

2、解析

 由上面示例可以看到,CountDownLatch主要是通过await()和countDown()进行操作的。来看看源码是如何实现的。

CountDownLatch的构造器需要传入一个计数作为参数,并且实例化了Sync对象,可以看到实例化Sync时,Sync的构造器将同步状态state设置为传入的参数count

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

CountDownLatch的实现依赖于AQS,它也是通过内部类继承AQS重写了一些主要的方法 

    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;//如果同步状态为0,获取成功,返回1,否则获取失败
        }

        //释放共享锁
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();//获取同步状态
                if (c == 0)//如果同步状态为0, 则不能再释放了
                    return false;
                int nextc = c-1;//否则,获取数量减1
                if (compareAndSetState(c, nextc))//更新同步状态
                    return nextc == 0;//当同步状态为0时,释放成功
            }
        }
    }

(1)await()方法

可以看到方法内部调用了AQS的acquireSharedInterruptibly方法,该方法会调用子类的tryAcquireShared方法,如果tryAcquireShared返回的值小于0,就会调用doAcquireSharedInterruptibly将线程插入同步队列中等待。

如果调用tryAcquireShared返回的值为1(也就是countdown使得state==0),获取成功,就会继续进行,失败则将调用await方法的线程放入同步队列。

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

(2)countDown()方法

countdown调用了AQS的releaseShared,实际上执行的是一个释放锁的操作,父类会调用子类的tryReleaseShared。

如果state为0,唤醒调用await方法被阻塞的线程。

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

二、CyclicBarrier(部分摘自Java并发系列[8]----CyclicBarrier源码分析 作者:劳夫子

利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。

1、先看一下CyclicBarrier的几个全局变量

    /**
     * 每一次使用栅栏代表一代.
     * 栅栏换代时generation也会改变或者重置
     */
    private static class Generation {
        boolean broken = false;
    }

    /**栅栏进入同步锁 */
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();
    /** 每次拦截的线程数 */
    private final int parties;
    /* 换代前执行的任务 */
    private final Runnable barrierCommand;
    /** 栅栏当前代 */
    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;

parties表示每次拦截的线程数,该值在构造时进行赋值。count是内部计数器,它的初始值和parties相同,以后随着每次await方法的调用而减1,直到减为0就将所有线程唤醒。可以看到CyclicBarrier的构造器:

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

CyclicBarrier有一个静态内部类Generation,该类的对象代表栅栏的当前代,就像玩游戏时代表的本局游戏,利用它可以实现循环等待。barrierCommand表示换代前执行的任务,当count减为0时表示本局游戏结束,需要转到下一局。在转到下一局游戏之前会将所有阻塞的线程唤醒,在唤醒所有线程之前你可以通过指定barrierCommand来执行自己的任务。

2、核心方法

在CyclicBarrier中实现了主要功能的方法是await()

    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final Generation g = generation;
            //1.检查栅栏是否被打破
            if (g.broken)
                throw new BrokenBarrierException();
            //2.如果线程被中断了,打破栅栏
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            int index = --count;//3.剩余等待数量-1
            if (index == 0) {  // 4.1数量为0时,进行换代
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();//换代前执行任务
                    ranAction = true;
                    nextGeneration();//换代
                    return 0;
                } finally {//finally语句会在try的return语句执行之后,return返回之前执行
                    //确保在任务未成功执行时能将所有线程唤醒
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // 4.2 剩余等待数量不为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();
                    }
                }

               /** 5.线程醒来后会执行下面三个判断,看看是否因为调用breakBarrier方法而被唤醒,如果是则抛出异常;看看是否是正常的换代操作而被唤醒,如果是则返回计数器的值;看看是否因为超时而被唤醒,如果是的话就调用breakBarrier打破栅栏并抛出异常。这里还需要注意的是,如果其中有一个线程因为等待超时而退出,那么整盘游戏也会结束,其他线程都会被唤醒。**/

                 //如果线程因为打翻栅栏操作而被唤醒则抛出异常
                if (g.broken)
                    throw new BrokenBarrierException();
                 //如果线程因为换代操作而被唤醒则返回计数器的值
                if (g != generation)
                    return index;
                //如果线程因为时间到了而被唤醒则打翻栅栏并抛出异常
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }
    /**
     * 设置栅栏当前代被打破,唤醒所有线程.
     * 该方法只会在持有锁时被调用
     */
    private void breakBarrier() {
        generation.broken = true;//更新broken标志
        count = parties;//重置count为parties
        trip.signalAll();//唤醒所有线程
    }
    /** 切换栅栏到下一代 */
    private void nextGeneration() {
        trip.signalAll();//已经完成,唤醒拦截的所有线程
        // set up next generation
        count = parties;//重置count的值为parties(需要拦截的线程数)
        generation = new Generation();//新建一代
    }

 三、总结(摘自Java并发系列[8]----CyclicBarrier源码分析 作者:劳夫子

CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值