Java并发-线程协同工具类(CountDownLatch 、CyclicBarrier、CompletionService)

目录

ReentrantLock、synchronized 大家经常使用也基本了解的比较多,今天介绍一下其他几个很棒的并发工具类

一、CountDownLatch 和 CyclicBarrier

例子背景:4个工作,开发工作1、开发工作2、测试、上线。

单线程执行所有的工作,整个执行顺序为串行,占用的资源少但是完成时长就会更长。

        

                                                                                             串行开发

	private Boolean WORK = Boolean.TRUE;

	@Test
	public void one() throws InterruptedException {
		Integer i = 0;
		//一直有工作
		while (WORK){
			long start = System.currentTimeMillis();
			System.out.println("######开始第" + (++i) +"次需求######");
			System.out.println("【开发工作1】开始");
			Thread.sleep(1000);
			System.out.println("【开发工作1】完成");

			System.out.println("【开发工作2】开始");
			Thread.sleep(3000);
			System.out.println("【开发工作2】完成");

			System.out.println("【测试】完成");
			System.out.println("【上线】完成");
			long use = System.currentTimeMillis() - start;
			System.out.println("######完成第" + i +"次需求用时:" + use + "######");

		}
	}

执行结果:

通过结果可以看到单线程执行基本耗时在:4008ms

🤔:产品上线的效率还能提高不?

👨‍💻‍:可以,我们多加几个人。请看CountDownLatch

1.1 CountDownLatch

为了提高处理效率我们可以开启多线程,但是需要注意一个问题那么就是开发工作1和开发工作2 要全部完成才能开始测试,所以需要让子线程与主线程的一个协同同步,即主线程等待多个子线程。

针对此类问题可以通过Java 的CountDownLatch 来完成线程间的协调。

         

                                                                                            多人协作

1.1.1 CountDownLatch使用

CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

CountDownLatch类只提供了一个构造器:

    /**
     * Constructs a {@code CountDownLatch} initialized with the given count.
     *
     * @param count the number of times {@link #countDown} must be invoked
     *        before threads can pass through {@link #await}
     * @throws IllegalArgumentException if {@code count} is negative
     */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

重要的三个操作方法:

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
		public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
////和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
//将count值减1
    public void countDown() {
        sync.releaseShared(1);
    }

 

了解了基本的方法,实现一下具体的Demo

/**
	 * 通过两个工作并行完成,但是需要全部完成后才可以提测,因此主线程需要等待两个线程才能开始继续
	 * 本例使用 CountDownLatch 来完成子线程与主线程的协同
	 * @throws InterruptedException
	 */
	@Test
	public void twoCountDownLatch() throws InterruptedException {
		final Integer[] i = {0};
		//一直有工作
		while (WORK){
			long start = System.currentTimeMillis();
			System.out.println("######开始第" + (++i[0]) +"次需求######");
			CountDownLatch countDownLatch = new CountDownLatch(2);
			//第一组开发工作
			new Thread(new Runnable() {
				@SneakyThrows
				@Override
				public void run() {
					System.out.println("第" + (i[0]) + "次【开发工作1】开始");
					Thread.sleep(1000);
					System.out.println("第" + (i[0]) + "次【开发工作1】完成");
					countDownLatch.countDown();

				}
			}).start();

			//第二组开发工作
			new Thread(new Runnable() {
				@SneakyThrows
				@Override
				public void run() {
					System.out.println("第" + (i[0]) + "次【开发工作2】开始");
					Thread.sleep(3000);
					System.out.println("第" + (i[0]) + "次【开发工作2】完成");
					countDownLatch.countDown();
				}
			}).start();

			countDownLatch.await();

			System.out.println("第" + (i[0]) + "次【测试】完成");
			System.out.println("第" + (i[0]) + "次【上线】完成");
			long use = System.currentTimeMillis() - start;
			System.out.println("######完成第" + (i[0]) +"次需求用时:" + use + "ms ######");

		}
	}

执行结果:

通过结果可以看到,让开发工作并行后明显缩短了每次产品的交付时间。由4008ms 减少至 3003ms

1.1.2 关键源码

CountDownLatch 内部定义了一个 Syns 类,Sync继承了大名鼎鼎的AQS(https://km.sankuai.com/page/155759840)。

/**
     * 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;
            }
        }
    }

 

🤔:还有没有办法再提高一些效率呢,人多了但是有一些时候空闲有点浪费?

👨‍💻‍:当然可以!美团的开发是宇宙最强😎💪,我们可以一个工作接着一个工作的干根本停不下来。

那么怎么提高RD的工作效率呢!请看CyclicBarrier

1.2 CyclicBarrier

通过上一工作流程我们可以看到在测试和上线阶段RD1和RD2 处于空闲状态,我们可以让上一个项目的测试和下一个产品开发进行并行。

提出一个假设前提:RD1 和 RD 2 要一起开始新工作,不能独自去开发新产品。

1.因此我们需要让RD1 和 RD 2 协同完成工作,不能RD1 完成一个工作就去开始下一个,需要等待RD2

2.需要在开发工作完成后能够通知测试工作开始

所以又要RD 互相通信等待不能独自去开发下一个产品,又要RD 能够通知QA 进行测试工作。因此可以使用 CyclicBarrier 来实现线程间的互相等待

                         

                                                                                            多人协作2.0

1.2.1 CyclicBarrier使用

CyclicBarrier字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了

CyclicBarrier 构造函数有两个。一个支持一次回环结束后进行动作的执行;另一个只是帮助完成多线程间的协同控制。参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。

    /**
     * Creates a new {@code CyclicBarrier} that will trip when the
     * given number of parties (threads) are waiting upon it, and which
     * will execute the given barrier action when the barrier is tripped,
     * performed by the last thread entering the barrier.
     *
     * @param parties the number of threads that must invoke {@link #await}
     *        before the barrier is tripped
     * @param barrierAction the command to execute when the barrier is
     *        tripped, or {@code null} if there is no action
     * @throws IllegalArgumentException if {@code parties} is less than 1
     */
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

    /**
     * Creates a new {@code CyclicBarrier} that will trip when the
     * given number of parties (threads) are waiting upon it, and
     * does not perform a predefined action when the barrier is tripped.
     *
     * @param parties the number of threads that must invoke {@link #await}
     *        before the barrier is tripped
     * @throws IllegalArgumentException if {@code parties} is less than 1
     */
    public CyclicBarrier(int parties) {
        this(parties, null);
    }

 

重要的三个操作方法:

类似于CountDownLatch,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));
    }

 

了解了基本的方法,实现一下具体的Demo

Executor executor =   Executors.newFixedThreadPool(1);

	@Test
	public void threeCyclicBarrier() throws InterruptedException {
		//计数器
		final Integer[] x = {0};
		final Integer[] y = {0};
		final Integer[] z = {0};
		final Integer[] j = {0};
		final Long[] start = new Long[10000];
		CyclicBarrier barrier = new CyclicBarrier(2,
				() -> {
					executor.execute(() ->
					{
						System.out.println("第" + (++j[0]) + "次【测试】完成");
						System.out.println("第" + (j[0]) + "次【上线】完成");
						long use = System.currentTimeMillis() - start[j[0]];
						System.out.println("######完成第" + j[0] + "次需求用时:" + use + "ms ######");
					});
				});

		//一直有工作
		while (WORK) {
			System.out.println("######开始第" + (++x[0]) + "次需求######");
			start[x[0]] = System.currentTimeMillis();
			//第一组开发工作
			new Thread(new Runnable() {
				@SneakyThrows
				@Override
				public void run() {
					System.out.println("第" + (++y[0]) + "次【开发工作1】开始");
					Thread.sleep(1000);
					System.out.println("第" + (y[0]) + "次【开发工作1】完成");
					barrier.await();

				}
			}).start();

			//第二组开发工作
			new Thread(new Runnable() {
				@SneakyThrows
				@Override
				public void run() {
					System.out.println("第" + (++z[0]) + "次【开发工作2】开始");
					Thread.sleep(3000);
					System.out.println("第" + (z[0]) + "次【开发工作2】完成");
					barrier.await();
				}
			}).start();
		}
	}

👨‍💻‍:效率实在太高了,一次完整产品才花费 1008ms

 

本质上还是提供并发的工作

1.2.2 关键源码

 

    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();


/**
     * Main barrier code, covering the various policies.
     */
    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();
        }
    }

通过CountDownLatch 也可以完成上述demo 的协同效果,但是需要我们管理每组工作的CountDownLatch ,进行多线程的数据交互管理,而CyclicBarrier则可以方便的“自动绑定”互相等待线程结束时的动作。 大家可以自己动手实现以下CountDownLatch 版的demo 完成宇宙最强开发的无缝衔接工作效率

举一个其他的例子缓解一下审美疲劳,思路可以用来实现上边的Demo

实现N个线程同时启动,然后依次打印各自的序号 😁

	@Test
	public void testThread(){
		int n = 10;
		CountDownLatch countDownLatchCur = new CountDownLatch(1);
		Te t = new Te(0, countDownLatchCur);
		Te tMain = t;
		tMain.start();
		for (int i = 1; i < n ; i++){
			t = new Te(i, t.getCountDownLatchMy()).start();
		}
		//测试一下打印最后一个。 but 第一个不开始,你真的打印不出来哦!
		t.start();
		//让第一个线程开始打印吧,大家都等半天了
		tMain.getCountDownLatch().countDown();
	}

	public class Te{
		Integer cur ;
		CountDownLatch countDownLatch;
		CountDownLatch countDownLatchMy = new CountDownLatch(1);

		public Te(Integer cur, CountDownLatch countDownLatch){
			this.cur = cur;
			this.countDownLatch = countDownLatch;
		}

		public Te start (){

			new Thread(() -> {
				try {
					countDownLatch.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(cur);
				countDownLatchMy.countDown();
			}).start();
			return this;
		}

		public CountDownLatch getCountDownLatchMy(){
			return this.countDownLatchMy;
		}
		public CountDownLatch getCountDownLatch(){
			return this.countDownLatch;
		}
	}

1.3 小结:

CountDownLatch 和 CyclicBarrier 是 Java 并发包提供的两个非常易用的线程同步工具类,这两个工具类用法的区别在这里还是有必要再强调一下:CountDownLatch 主要用来解决一个线程等待多个线程的场景,可以类比一个产品上线,只有各个依赖服务都部署成功才能开启新产品的操作入口;而 CyclicBarrier 是一组线程之间互相等待,类似家庭聚会人齐了才会开饭。除此之外 CountDownLatch 的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await(),该线程会直接通过。但 CyclicBarrier 的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到 0 会自动重置到你设置的初始值。除此之外,CyclicBarrier 还可以设置回调函数,可以说是功能丰富。

 

二、CompletionService

2.1 CompletionService使用

CompletionService 接口的实现类是 ExecutorCompletionService,这个实现类的构造方法有两个,分别是:ExecutorCompletionService(Executor executor);ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)。这两个构造方法都需要传入一个线程池,如果不指定 completionQueue,那么默认会使用无界的 LinkedBlockingQueue。任务执行结果的 Future 对象就是加入到 completionQueue 中。

    /**
     * Creates an ExecutorCompletionService using the supplied
     * executor for base task execution and a
     * {@link LinkedBlockingQueue} as a completion queue.
     *
     * @param executor the executor to use
     * @throws NullPointerException if executor is {@code null}
     */
    public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }

    /**
     * Creates an ExecutorCompletionService using the supplied
     * executor for base task execution and the supplied queue as its
     * completion queue.
     *
     * @param executor the executor to use
     * @param completionQueue the queue to use as the completion queue
     *        normally one dedicated for use by this service. This
     *        queue is treated as unbounded -- failed attempted
     *        {@code Queue.add} operations for completed tasks cause
     *        them not to be retrievable.
     * @throws NullPointerException if executor or completionQueue are {@code null}
     */
    public ExecutorCompletionService(Executor executor,
                                     BlockingQueue<Future<V>> completionQueue) {
        if (executor == null || completionQueue == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = completionQueue;
    }

CompletionService 的实现原理是内部维护了一个阻塞队列,当任务执行结束就把任务的执行结果加入到阻塞队列中,把任务执行结果的 Future 对象加入到阻塞队列中。

    private final BlockingQueue<Future<V>> completionQueue;  

		public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task);
        executor.execute(new QueueingFuture<V>(f, completionQueue));
        return f;
    }

Demo中,我们没有指定 completionQueue,因此默认使用无界的 LinkedBlockingQueue。

通过 CompletionService 接口提供的 submit() 方法提交了三个查询地理位置信息操作,这三个操作将会被 CompletionService 异步执行。最后,我们通过 CompletionService 接口提供的 take() 方法获取一个 Future 对象,调用 Future 对象的 get() 方法就能返回查询地理位置信息操作的执行结果了。

CompletionService 的 3 个方法take()、poll() 都是和阻塞队列相关的,都是从阻塞队列中获取并移除一个元素;它们的区别在于如果阻塞队列是空的,那么调用 take() 方法的线程会被阻塞,而 poll() 方法会返回 null 值。 poll(long timeout, TimeUnit unit) 方法支持以超时的方式获取并移除阻塞队列头部的一个元素,如果等待了 timeout unit 时间,阻塞队列还是空的,那么该方法会返回 null 值。

/**
*伪代码,无法执行	
*/

//同时查询多个地图服务,然后拼装一个准确的地理位置信息描述
	private Executor executor;
	@Test
	public void testC(){
		CompletionService<Map<String, String>> cs = new ExecutorCompletionService<>(executor);
		Map<String, String> selectMap = new ConcurrentHashMap<>();
		selectMap.put("百度地图", "地点");
		selectMap.put("腾讯地图", "地点");
		selectMap.put("高德地图", "地点");

		for (Map.Entry<String, String> entry : selectMap.entrySet()) {
			cs.submit(() -> seletcMapService.select(entry.getKey(), entry.getValue());
		}
		Map<String, MapInfo> MapInfoMap = new ConcurrentHashMap<>();

		for (int i = 0; i <  selectMap.size(); i++) {
			try {
				// CompletionService 内部的 BlockingQueue<Future<V>> completionQueue;
				MapInfo mapInfo = cs.take().get();
				MapInfoMap.put(mapInfo.getkey(), mapInfo);
			} catch (InterruptedException | ExecutionException e) {
				Thread.currentThread().interrupt();
				log.error("查询数据异常",e);
			}
		}
	}

2.2 小结

除了CompletionService,我们更经常使用Stream,那么到底应该选哪个呢,引用《Java 8实战》一段知识。

摘自:Java8 实战-Page-234

 

参考:

《Java 并发编程》

https://time.geekbang.org/column/article/89461

https://time.geekbang.org/column/article/92245

https://www.cnblogs.com/dolphin0520/p/3920397.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章