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

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