CountDownLatch和CyclicBarrier区别和用法

目录

1.概念解释

CountDownLatch概念:

CountDownLatch概念从源码分析:

CyclicBarrier概念

CyclicBarrier概念从源码分析

概念总结

2.构造器

3.代码验证二者在实际中的不同

4.归纳总结

5.网上的一些误区


CountDownLatch和CyclicBarrier都是juc下的并发工具类,二者功能在处理某些事情下看着很相似:都是阻塞线程,但是如果细品和查看源码的话会发现二者之间还是有区别的:

CountDownLatch主要是阻塞主线程,等待多线程执行完成之后再执行主线程await之后的代码片段,侧重点是主线程等待子线程(多线程)完成之后被唤醒。

CyclicBarrier主要是每个多线程在某一刻阻塞,然后各个多线程之间相互等待,直到最后一个多线程被阻塞,然后冲破栅栏,各自执行自己await()之后的代码段(另一个写法是用两个参数的构造去共完成另一个Runnable任务),侧重点是多线程之间的相互等待。

另外,CountDownLatch是一次性,而CyclicBarrier是可重复利用的(查看源码可以发现当最后一道栅栏被冲破之后,如果还需要用到的话会重新new Generation栅栏对象)。

了解了这两个类的侧重点之后,才更好选择合适并发工具类。下面就通过例子和源码,来验证这些区别。

1.概念解释

CountDownLatch概念:

count为计数,down减少,latch闩,所以CountDownLatch大致可以翻译为倒计时闭锁(闩)。从单词中可以很明显的得到以下信息:通过倒计时或倒数计数实现的闭锁,来实现线程的等待。

CountDownLatch概念从源码分析:

先看下该类的部分源码

     /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    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;
            }
        }
    }

    private final Sync sync;

源码中可以看到,CountDownLatch类中有个Sync的同步类,而Sync又继承了抽象队列同步器AbstractQueuedSynchronizer,另外看到AcquireShared又应该可以猜到是基于共享锁模式。所以说CountDownLatch是基于AQS的共享锁模式。即解释了CountDownLatch是基于AQS的共享锁模式的倒计时(数)闭锁(闩)。

CyclicBarrier概念

cyclic循环,barrier栅栏,所以CyclicBarrier被翻译成循环栅栏。从单词中可以得到的信息是:通过栅栏的方式将一组线程阻塞,并且可以循环使用。

CyclicBarrier概念从源码分析

先看下该类的部分源码

    /**
     * Each use of the barrier is represented as a generation instance.
     * The generation changes whenever the barrier is tripped, or
     * is reset. There can be many generations associated with threads
     * using the barrier - due to the non-deterministic way the lock
     * may be allocated to waiting threads - but only one of these
     * can be active at a time (the one to which {@code count} applies)
     * and all the rest are either broken or tripped.
     * There need not be an active generation if there has been a break
     * but no subsequent reset.
     */
    private static class Generation {
        boolean broken = false;
    }

    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    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;

源码中可以看到CyclicBarrier是通过ReentrantLock和锁的Contatin来实现的(await方法调用doawait方法,里面通过ReentrantLock实现的),然后再看ReentrantLock的部分源码

    private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

可以看到ReentrantLock也含有Sync类型的变量,并且还是通过CAS操作来setExclusiveOwnerThread(current)的独占锁。所以说CyclicBarrier是基于AQS的独占锁模式。

概念总结:

CyclicBarrier是基于AQS的独占锁模式实现的阻塞线程,CoutDownLatch是基于AQS的共享锁模式阻塞线程。

2.构造器

CountDownLatch只有一个构造器public CountDownLatch(int count)

CyclicBarrier有两个构造器,分别是public CyclicBarrier(int parties, Runnable barrierAction)和public CyclicBarrier(int parties)

3.代码验证二者在实际中的不同

CountDownLatch用法

为了验证先后顺序,所以开启了两个线程并且sleep的时间不同,来模拟两个业务。这样更能看出效果

import java.util.Date;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

	public static void main(String[] args) {
		CountDownLatch cdl = new CountDownLatch(2);
		try {
			for (int i = 0; i < 2; i++) {
				new Thread() {
					public void run() {
						long thread1Id = Thread.currentThread().getId();
						try {
							Date date1 = new Date();
							String szDate = String.format("当前时间为:%tH时%tM分%tS秒", date1, date1, date1);
							System.out.println("线程ID:" + thread1Id + "正在执行,时间:" + szDate);
							int r = (int) (Math.random() * 10000);
							Thread.sleep(Long.parseLong(r+""));
							Date date2 = new Date();
							String szDate2 = String.format("当前时间为:%tH时%tM分%tS秒", date2, date2, date2);
							System.out.println("线程ID:" + thread1Id + "  over" + szDate2);
							cdl.countDown();
							System.out.println("此时计数=" + cdl.getCount());
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}.start();
			}
			cdl.await();
			System.out.println("两个线程全部执行完成");
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

执行结果:

线程ID:10正在执行,时间:当前时间为:10时43分56秒
线程ID:11正在执行,时间:当前时间为:10时43分56秒
线程ID:11  over当前时间为:10时44分03秒
此时计数=1
线程ID:10  over当前时间为:10时44分05秒
此时计数=0
两个线程全部执行完成

结果分析

1.cdl.countDown()方法是将初始值递减。

2.主线程在调用了cdl.await();的方法之后一直在等待最后一个时间长的线程执行完成才会执行。

归纳:CountDownLatch是设置一个初始量N,每个线程中调用了countDown()方法之后,计数器变为N-1,直到计数为0的时候,调用await()的主线程才会获得锁资源执行。也就是说CountDownLatch是让一个线程(主线程)等待其他多线程全部执行完成之后再执行。

思维扩展:(感兴趣的可以自己试试)

1.在主线程调用await()的try语句块中,并且在await()之上加入一些其他的代码,会发现这些代码和两个多线程是同步执行的。也就是说只有在调用了await方法之后主线程才会被阻塞。也就是说主线程在await()下面的代码块才会被阻塞,等待获取锁。如果直接把await()方法的调用放到new CountDownLatch()之后里面执行,那惨了,后面全被阻塞了,等着去吧……

2.在多线程调用countDown()方法方法之后,再用sleep(10000),然后再sysout输出一些东西的话,会发现主线程还是会在工作时间长的多线程执行完countDown()之后会立即执行,而并不会再等10秒钟才去执行。这说明主线程cdl.getCount()等于0的时候会立马执行,不会管多线程调用countDown()之后的内容在干什么。

CyclicBarrier用法

为了验证先后顺序,所以开启了3个线程并且sleep的时间不同,来模拟三个业务。这样更能看出效果

import java.util.Date;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
	public static void main(String[] args) {
		CyclicBarrier cb = new CyclicBarrier(3);
		for (int i = 0; i < 3; i++) {
			new Thread() {
				public void run() {
					long thread1Id = Thread.currentThread().getId();
					try {
						Date date1 = new Date();
						String szDate = String.format("当前时间为:%tH时%tM分%tS秒", date1, date1, date1);
						System.out.println("线程ID:" + thread1Id + "正在准备中,时间:" + szDate);
						int r = (int) (Math.random() * 10000);
						Thread.sleep(Long.parseLong(r + ""));
						Date date2 = new Date();
						String szDate2 = String.format("当前时间为:%tH时%tM分%tS秒", date2, date2, date2);
						System.out.println("线程ID:" + thread1Id + "  准备好了" + szDate2);
						try {
							System.out.println("线程ID:" + thread1Id + "到达栅栏,此时前面已经阻塞了的线程数" + cb.getNumberWaiting());
							cb.await();
							System.out.println("线程ID:" + thread1Id + "可以开始了");
						} catch (BrokenBarrierException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}.start();
		}
		Date date1 = new Date();
		String szDate = String.format("当前时间为:%tH时%tM分%tS秒", date1, date1, date1);
		System.out.println("主程序运行:" + szDate);
	}
}

运行结果:

主程序运行:当前时间为:10时46分12秒
线程ID:11正在准备中,时间:当前时间为:10时46分12秒
线程ID:12正在准备中,时间:当前时间为:10时46分12秒
线程ID:10正在准备中,时间:当前时间为:10时46分12秒
线程ID:12  准备好了当前时间为:10时46分12秒
线程ID:12到达栅栏,此时前面已经阻塞了的线程数0
线程ID:10  准备好了当前时间为:10时46分16秒
线程ID:10到达栅栏,此时前面已经阻塞了的线程数1
线程ID:11  准备好了当前时间为:10时46分17秒
线程ID:11到达栅栏,此时前面已经阻塞了的线程数2
线程ID:11可以开始了
线程ID:12可以开始了
线程ID:10可以开始了

结果分析

1.各线程到达栅栏的时候都会被阻塞,等待其他多线程也被阻塞

2.当所有的多线程都被阻塞之后会并发执行被await()之后的代码段

3.多线程和主线程是同时执行的,也就是说CyclicBarrier阻塞的是每个多线程,让多线程相互等待,直到所有的多线程都执行完await()之前的代码,与主线程无关。

归纳:

CyclicBarrier是用于多线程之间的相互等待,直到所有的多线程都到达阻塞的栅栏,然后所有多线程全部被唤醒,并发执行各自后面的代码。这个阻塞是与主线程无关的。

思维扩展:

上面已经说过了可以用CyclicBarrier的两个构造的方法,这样更方便各自执行完自己的操作后,共同去执行某个任务。

举个例子,3个程序员对产品设计的产品有意见,一个人说话产品觉得是你的问题,但是3个人一起去那就得跟他好好谈谈了。

import java.util.Date;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo2 {

	public static void main(String[] args) {
		CyclicBarrier cb = new CyclicBarrier(3, new Runnable() {
			public void run() {
				Date date1 = new Date();
				String szDate = String.format("当前时间为:%tH时%tM分%tS秒", date1, date1, date1);
				System.out.println("既然都到了,那就一起去找产品谈谈吧,时间:" + szDate);
			}
		});
		for (int i = 0; i < 3; i++) {
			new Thread() {
				public void run() {
					long thread1Id = Thread.currentThread().getId();
					try {
						Date date1 = new Date();
						String szDate = String.format("当前时间为:%tH时%tM分%tS秒", date1, date1, date1);
						System.out.println("程序员:" + thread1Id + "正在准备中,时间:" + szDate);
						int r = (int) (Math.random() * 10000);
						Thread.sleep(Long.parseLong(r + ""));
						Date date2 = new Date();
						String szDate2 = String.format("当前时间为:%tH时%tM分%tS秒", date2, date2, date2);
						System.out.println("程序员:" + thread1Id + "  准备好了" + szDate2);
						try {
							System.out.println(
									"程序员:" + thread1Id + "到达栅栏,此时前面已经来了(被阻塞了)的程序员人数(线程数)" + cb.getNumberWaiting());
							cb.await();
						} catch (BrokenBarrierException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}.start();
		}
	}
}

运行结果:

程序员:10正在准备中,时间:当前时间为:11时06分59秒
程序员:11正在准备中,时间:当前时间为:11时06分59秒
程序员:12正在准备中,时间:当前时间为:11时06分59秒
程序员:12  准备好了当前时间为:11时07分01秒
程序员:12到达栅栏,此时前面已经来了(被阻塞了)的程序员人数(线程数)0
程序员:10  准备好了当前时间为:11时07分04秒
程序员:10到达栅栏,此时前面已经来了(被阻塞了)的程序员人数(线程数)1
程序员:11  准备好了当前时间为:11时07分06秒
程序员:11到达栅栏,此时前面已经来了(被阻塞了)的程序员人数(线程数)2
既然都到了,那就一起去找产品谈谈吧,时间:当前时间为:11时07分06秒

结果可以看出,当3个程序员都准备好开战了,就去找产品交流了。

4.归纳总结

共同点:都是阻塞线程

不同点:CountDownLatch是主线程被多线程阻塞,直到多线程执行完成才被唤醒继续执行,所以更关注主线程等待多线程执行完成再继续执行的场景;CyclicBarrier是多线程各自被阻塞在栅栏前,是多线程之间的相互等待,直到全部的多线程全部执行完成,然后并发的去共同做某件事,比如:赛跑比赛的时候,需要等所有运动员都准备完成之后,才能开始进行比赛。

所以在用的时候还需要对症下药。

5.网上的一些误区

看过一些别的文章,CountDownLatch是通过计数器递减没毛病,但是CyclicBarrier有人说递减,也有人递增,这样就很容易误导,所以碰到这种情况,还是要自己看看源码,然后动动手测试下。所以我在这里解释下为什么对于CyclicBarrier有不同的说法,其实他们都是从自己的思路出发总结的,不能说不对。

对于CyclicBarrier,内部的原理是等待所有线程被阻塞,所以对于被阻塞的线程数来说是递增的,但是源码上有这么一段

await()方法:

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

dowait()方法:

    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();
            }

            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
            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();
                    }
                }

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

                if (g != generation)
                    return index;

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

意思是说每当有线程到达后,剩余没有起作用的栅栏数int index = --count(也就是说可用栅栏数执行了--操作);所以说递减也没毛病,针对当前栅栏数是递减的。

所以这里就算对迷惑的朋友或许有所帮助。

你可能感兴趣的:(多线程,高并发,juc,多线程,并发编程)