主頁 > 後端開發 > CountDownLatch和CyclicBarrier 傻傻的分不清?超長精美圖文又來了

CountDownLatch和CyclicBarrier 傻傻的分不清?超長精美圖文又來了

2020-10-04 07:58:06 後端開發

  • 你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star


前言

并發編程的三大核心是分工同步互斥,在日常開發中,經常會碰到需要在主執行緒中開啟多個子執行緒去并行的執行任務,并且主執行緒需要等待所有子執行緒執行完畢再進行匯總的場景,這就涉及到分工與同步的內容了

在講 有序性可見性,Happens-before來搞定 時,提到過 join() 規則,使用 join() 就可以簡單的實作上述場景:

@Slf4j
public class JoinExample {

	public static void main(String[] args) throws InterruptedException {
		Thread thread1 = new Thread(() -> {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				log.info("Thread-1 執行完畢");
			}
		}, "Thread-1");

		Thread thread2 = new Thread(() -> {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				log.info("Thread-2 執行完畢");
			}
		}, "Thread-2");

		thread1.start();
		thread2.start();

		thread1.join();
		thread2.join();

		log.info("主執行緒執行完畢");
	}
}

運行結果:

整個程序可以這么理解

我們來查看 join() 的實作原始碼:

其實作原理是不停的檢查 join 執行緒是否存活,如果 join 執行緒存活,則 wait(0) 永遠的等下去,直至 join 執行緒終止后,執行緒的 this.notifyAll() 方法會被呼叫(該方法是在 JVM 中實作的,JDK 中并不會看到原始碼),退出回圈恢復主執行緒執行,很顯然這種回圈檢查的方式比較低效

除此之外,使用 join() 缺少很多靈活性,比如實際專案中很少讓自己單獨創建執行緒(原因在 我會手動創建執行緒,為什么要使用執行緒池? 中說過)而是使用 Executor, 這進一步減少了 join() 的使用場景,所以 join() 的使用在多數是停留在 demo 演示上

那如何實作文中開頭提到的場景呢?

CountDownLatch

CountDownLatch, 直譯過來【數量向下門閂】,那肯定里面有計數器的存在了,我們將上述程式用 CountDownLatch 實作一下,先讓大家有個直觀印象

@Slf4j
public class CountDownLatchExample {

	private static CountDownLatch countDownLatch = new CountDownLatch(2);

	public static void main(String[] args) throws InterruptedException {
		// 這里不推薦這樣創建執行緒池,最好通過 ThreadPoolExecutor 手動創建執行緒池
		ExecutorService executorService = Executors.newFixedThreadPool(2);

		executorService.submit(() -> {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				log.info("Thread-1 執行完畢");
				//計數器減1
				countDownLatch.countDown();
			}
		});

		executorService.submit(() -> {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				log.info("Thread-2 執行完畢");
				//計數器減1
				countDownLatch.countDown();
			}
		});

		log.info("主執行緒等待子執行緒執行完畢");
		log.info("計數器值為:" + countDownLatch.getCount());
		countDownLatch.await();
		log.info("計數器值為:" + countDownLatch.getCount());
		log.info("主執行緒執行完畢");
		executorService.shutdown();
	}
}

運行結果如下:

結合上述示例的運行結果,相信你也能猜出 CountDownLatch 的實作原理了:

  1. 初始化計數器數值,比如為2
  2. 子執行緒執行完則呼叫 countDownLatch.countDown() 方法將計數器數值減1
  3. 主執行緒呼叫 await() 方法阻塞自己,直至計數器數值為0(即子執行緒全部執行結束)

不知道你是否注意,countDownLatch.countDown(); 這行代碼可以寫在子執行緒執行的任意位置,不像 join() 要完全等待子執行緒執行完,這也是 CountDownLatch 靈活性的一種體現

上述的例子還是過于簡單,Oracle 官網 CountDownLatch 說明 有兩個非常經典的使用場景,示例很簡單,強烈建議查看相關示例代碼,打開使用思路,我將兩個示例代碼以圖片的形式展示在此處:

官網示例1

  • 第一個是開始信號 startSignal,阻止任何工人 Worker 繼續作業,直到司機 Driver 準備好讓他們繼續作業
  • 第二個是完成信號 doneSignal,允許司機 Driver 等待,直到所有的工人 Worker 完成,

官網示例2

另一種典型的用法是將一個問題分成 N 個部分 (比如將一個大的 list 拆分成多分,每個 Worker 干一部分),Worker 執行完自己所處理的部分后,計數器減1,當所有子部分完成后,Driver 才繼續向下執行

結合官網示例,相信你已經可以結合你自己的業務場景解,通過 CountDownLatch 解決一些串行瓶頸來提高運行效率了,會用還遠遠不夠,咱得知道 CountDownLatch 的實作原理

原始碼分析

CountDownLatch 是 AQS 實作中的最后一個內容,有了前序文章的知識鋪墊:

  • Java AQS佇列同步器以及ReentrantLock的應用
  • Java AQS共享式獲取同步狀態及Semaphore的應用分析

當你看到 CountDownLatch 的原始碼內容,你會高興的笑起來,內容真是太少了

展開類結構全部內容就這點東西

既然 CountDownLatch 是基于 AQS 實作的,那肯定也離不開對同步狀態變數 state 的操作,我們在初始化的時候就將計數器的值賦值給了state

另外,它可以多個執行緒同時獲取,那一定是基于共享式獲取同步變數的用法了,所以它需要通過重寫下面兩個方法控制同步狀態變數 state :

  • tryAcquireShared()
  • tryReleaseShared()

CountDownLatch 暴露給使用者的只有 await()countDown() 兩個方法,前者是阻塞自己,因為只有獲取同步狀態才會才會出現阻塞的情況,那自然是在 await() 的方法內部會用到 tryAcquireShared();有獲取就要有釋放,那后者 countDown() 方法內部自然是要用到 tryReleaseShared() 方法了

PS:如果你對上面這個很自然的推斷理解有困難,強烈建議你看一下前序文章的鋪墊,以防止知識斷層帶來的困擾

await()

先來看 await() 方法, 從方法簽名上看,該方法會拋出 InterruptedException, 所以它是可以回應中斷的,這個我們在 Java多執行緒中斷機制 中明確說明過

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

其內部呼叫了同步器提供的模版方法 acquireSharedInterruptibly

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
  	// 如果監測到中斷標識為true,會重置標識,然后拋出 InterruptedException
    if (Thread.interrupted())
        throw new InterruptedException();
  	// 呼叫重寫的 tryAcquireShared 方法,該方法結果如果大于零則直接回傳,程式繼續向下執行,如果小于零,則會阻塞自己
    if (tryAcquireShared(arg) < 0)
      	// state不等于0,則嘗試阻塞自己
        doAcquireSharedInterruptibly(arg);
}

重寫的 tryAcquireShared 方法非常簡單, 就是判斷同步狀態變數 state 的值是否為 0, 如果為零 (子執行緒已經全部執行完畢)則回傳1, 否則回傳 -1

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

如果子執行緒沒有全部執行完畢,則會通過 doAcquireSharedInterruptibly 方法阻塞自己,這個方法在 Java AQS共享式獲取同步狀態及Semaphore的應用分析 中已經仔細分析過了,這里就不再贅述了

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
              	// 再次嘗試獲取同步裝阿嚏,如果大于0,說明子執行緒全部執行完畢,直接回傳
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
          	// 阻塞自己
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

await() 方法的實作就是這么簡單,接下來看看 countDown() 的實作原理

countDown()

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

同樣是呼叫同步器提供的模版方法 releaseShared

public final boolean releaseShared(int arg) {
  	// 呼叫自己重寫的同步器方法
    if (tryReleaseShared(arg)) {
      	// 喚醒呼叫 await() 被阻塞的執行緒
        doReleaseShared();
        return true;
    }
    return false;
}

重寫的 tryReleaseShared 同樣很簡單

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
      	// 如果當前狀態值為0,則直接回傳 (1)
        if (c == 0)
            return false;
      	// 使用 CAS 讓計數器的值減1 (2)
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

代碼 (1) 判斷當前同步狀態值,如果為0 則直接回傳 false;否則執行代碼 (2),使用 CAS 將計數器減1,如果 CAS 失敗,則回圈重試,最侄訓傳 nextc == 0 的結果值,如果該值回傳 true,說明最后一個執行緒已呼叫 countDown() 方法,然后就要喚醒呼叫 await() 方法被阻塞的執行緒,同樣由于分析過 AQS 的模版方法 doReleaseShared 整個釋放同步狀態以及喚醒的程序,所以這里同樣不再贅述了

仔細看CountDownLatch重寫的 tryReleaseShared 方法,有一點需要和大家說明:

代碼 (1) if (c == 0) 看似沒什么用處,其實用處大大滴,如果沒有這個判斷,當計數器值已經為零了,其他執行緒再呼叫 countDown 方法會將計數器值變為負值

現在就差 await(long timeout, TimeUnit unit) 方法沒介紹了

await(long timeout, TimeUnit unit)

public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

該方法簽名同樣拋出 InterruptedException,意思可回應中斷,它其實就是 await() 更完善的一個版本,簡單來說就是

主執行緒設定等待超時時間,如果該時間內子執行緒沒有執行完畢,主執行緒也會直接回傳

我們將上面的例子稍稍修改一下你就會明白(主執行緒超時時間設定為 2 秒,而子執行緒要 sleep 5 秒)

@Slf4j
public class CountDownLatchTimeoutExample {

   private static CountDownLatch countDownLatch = new CountDownLatch(2);

   public static void main(String[] args) throws InterruptedException {
      // 這里不推薦這樣創建執行緒池,最好通過 ThreadPoolExecutor 手動創建執行緒池
      ExecutorService executorService = Executors.newFixedThreadPool(2);

      executorService.submit(() -> {
         try {
            Thread.sleep(5000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         } finally {
            log.info("Thread-1 執行完畢");
            //計數器減1
            countDownLatch.countDown();
         }
      });

      executorService.submit(() -> {
         try {
            Thread.sleep(5000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         } finally {
            log.info("Thread-2 執行完畢");
            //計數器減1
            countDownLatch.countDown();
         }
      });

      log.info("主執行緒等待子執行緒執行完畢");
      log.info("計數器值為:" + countDownLatch.getCount());
      countDownLatch.await(2, TimeUnit.SECONDS);
      log.info("計數器值為:" + countDownLatch.getCount());
      log.info("主執行緒執行完畢");
      executorService.shutdown();
   }
}

運行結果如下:

形象化的展示上述示例的運行程序

小結

CountDownLatch 的實作原理就是這么簡單,了解了整個實作程序后,你也許發現了使用 CountDownLatch 的一個問題:

計數器減 1 操作是一次性的,也就是說當計數器減到 0, 再有執行緒呼叫 await() 方法,該執行緒會直接通過,不會再起到等待其他執行緒執行結果起到同步的作用了

為了解決這個問題,貼心的 Doug Lea 大師早已給我們準備好相應策略 CyclicBarrier

本來想將 CyclicBarrier 的內容放到下一個章節,但是 CountDownLatch 的內容著實有些少,不夠解渴,另外有對比才有傷害,所以內容沒結束,咱得繼續看 CyclicBarrier

CyclicBarrier

上面簡單說了一下 CyclicBarrier 被創造出來的理由,這里先看一下它的字面解釋:

概念總是有些抽象,我們將上面的例子用 CyclicBarrier 再做個改動,先讓大家有個直觀的使用概念

@Slf4j
public class CyclicBarrierExample {

   // 創建 CyclicBarrier 實體,計數器的值設定為2
   private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

   public static void main(String[] args) {
      ExecutorService executorService = Executors.newFixedThreadPool(2);
      int breakCount = 0;

     	// 將執行緒提交到執行緒池
      executorService.submit(() -> {
         try {
            log.info(Thread.currentThread() + "第一回合");
            Thread.sleep(1000);
            cyclicBarrier.await();

            log.info(Thread.currentThread() + "第二回合");
            Thread.sleep(2000);
            cyclicBarrier.await();

            log.info(Thread.currentThread() + "第三回合");
         } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
         } 
      });

      executorService.submit(() -> {
         try {
            log.info(Thread.currentThread() + "第一回合");
            Thread.sleep(2000);
            cyclicBarrier.await();

            log.info(Thread.currentThread() + "第二回合");
            Thread.sleep(1000);
            cyclicBarrier.await();

            log.info(Thread.currentThread() + "第三回合");
         } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
         }
      });

      executorService.shutdown();
   }

}

運行結果:

結合程式代碼與運行結果,我們可以看出,子執行緒執行完第一回合后(執行回合所需時間不同),都會呼叫 await() 方法,等所有執行緒都到達屏障點后,會突破屏障繼而執行第二回合,同樣的道理最終到達第三回合

形象化的展示上述示例的運行程序

看到這里,你應該明白 CyclicBarrier 的基本用法,但隨之你內心也應該有了一些疑問:

  1. 怎么判斷所有執行緒都到達屏障點的?
  2. 突破某一屏障后,又是怎么重置 CyclicBarrier 計數器,等待執行緒再一次突破屏障呢?

帶著這些問題我們來看一看原始碼

原始碼分析

同樣先打開 CyclicBarrier 的類結構,展開類全部內容,其實也沒多少內容

從類結構中看到有:

  1. await() 方法,猜測應該和 CountDownLatch 是類似的,都是獲取同步狀態,阻塞自己
  2. ReentrantLock,CyclicBarrier 內部竟然也用到了我們之前講過的 ReentrantLock,猜測這個鎖一定保護 CyclicBarrier 的某個變數,那肯定也是基于 AQS 相關知識了
  3. Condition,存在條件,猜測會有等待/通知機制的運用

我們繼續帶著這些猜測,結合上面的實體代碼一點點來驗證

// 創建 CyclicBarrier 實體,計數器的值設定為2
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

查看建構式 (這里的英文注釋舍不得刪掉,因為說的太清楚了,我來結合注釋來說明一下):

private final int parties;
private int count;

public CyclicBarrier(int parties) {
    this(parties, null);
}

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

根據注釋說明,parties 代表沖破屏障之前要觸發的執行緒總數,count 本身又是計數器,那問題來了

直接就用 count 不就可以了嘛?為啥同樣用于初始化計數器,要維護兩個變數呢?

從 parties 和 count 的變數宣告中,你也能看出一些門道,前者有 final 修飾,初始化后就不可以改變了,因為 CyclicBarrier 的設計目的是可以回圈利用的,所以始終用 parties 來記錄執行緒總數,當 count 計數器變為 0 后,如果沒有 parties 的值賦給它,怎么進行重新復用再次計數呢,所以這里維護兩個變數很有必要

接下來就看看 await() 到底是怎么實作的

// 從方法簽名上可以看出,該方法同樣可以被中斷,另外還有一個 BrokenBarrierException 例外,我們一會看
public int await() throws InterruptedException, BrokenBarrierException {
    try {
      	// 呼叫內部 dowait 方法, 第一個引數為 false,表示不設定超時時間,第二個引數也就沒了意義
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

接下來看看 dowait(false, 0L) 做了哪些事情 (這個方法內容有點多,別擔心,邏輯并不復雜,請看關鍵代碼注釋)

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    final ReentrantLock lock = this.lock;
    // 還記得之前說過的 Lock 標準范式嗎? JDK 內部都是這么使用的,你一定也要遵循范式
    lock.lock();
    try {
        final Generation g = generation;

      	// broken 是靜態內部類 Generation唯一的一個成員變數,用于記錄當前屏障是否被打破,如果打破,則拋出 BrokenBarrierException 例外
      	// 這里感覺挺困惑的,我們要【沖破】屏障,這里【打破】屏障卻拋出例外,注意我這里的用詞
        if (g.broken)
            throw new BrokenBarrierException();

      	// 如果執行緒被中斷,則會通過 breakBarrier 方法將 broken 設定為true,也就是說,如果有執行緒收到中斷通知,直接就打破屏障,停止 CyclicBarrier, 并喚醒所有執行緒
        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }
      
      	// ************************************
      	// 因為 breakBarrier 方法在這里會被呼叫多次,為了便于大家理解,我直接將 breakBarrier 代碼插入到這里
      	private void breakBarrier() {
          // 將打破屏障標識 設定為 true
          generation.broken = true;
          // 重置計數器
          count = parties;
          // 喚醒所有等待的執行緒
          trip.signalAll();
    		}
      	// ************************************

				// 每當一個執行緒呼叫 await 方法,計數器 count 就會減1
        int index = --count;
      	// 當 count 值減到 0 時,說明這是最后一個呼叫 await() 的子執行緒,則會突破屏障
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
              	// 獲取建構式中的 barrierCommand,如果有值,則運行該方法
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
              	// 激活其他因呼叫 await 方法而被阻塞的執行緒,并重置 CyclicBarrier
                nextGeneration();
              
                // ************************************
                // 為了便于大家理解,我直接將 nextGeneration 實作插入到這里
                private void nextGeneration() {
                    // signal completion of last generation
                    trip.signalAll();
                    // set up next generation
                    count = parties;
                    generation = new Generation();
                }
                // ************************************
              
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

      	// index 不等于0, 說明當前不是最后一個執行緒呼叫 await 方法
        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
              	// 沒有設定超時時間
                if (!timed)
                  	// 進入條件等待
                    trip.await();
                else if (nanos > 0L)
                  	// 否則,判斷超時時間,這個我們在 AQS 中有說明過,包括為什么最后超時閾值 spinForTimeoutThreshold 不再比較的原因,大家會看就好
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
              	// 條件等待被中斷,則判斷是否有其他執行緒已經使屏障破壞,若沒有則進行屏障破壞處理,并拋出例外;否則再次中斷當前執行緒

                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    Thread.currentThread().interrupt();
                }
            }

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

          	// 如果新一輪回環結束,會通過 nextGeneration 方法新建 generation 物件
            if (g != generation)
                return index;

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

doWait 就是 CyclicBarrier 的核心邏輯, 可以看出,該方法入口使用了 ReentrantLock,這也就是為什么 Generation broken 變數沒有被宣告為 volatile 型別保持可見性,因為對其的更改都是在鎖的內部,同樣在鎖的內部對計數器 count 做更新,也保證了原子性

doWait 方法中,是通過 nextGeneration 方法來重新初始化/重置 CyclicBarrier 狀態的,該類中還有一個 reset() 方法,也是重置 CyclicBarrier 狀態的

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() 方法并沒有在 CyclicBarrier 內部被呼叫,顯然是給 CyclicBarrier 使用者來呼叫的,那問題來了

什么時候呼叫 reset() 方法呢

正常情況下,CyclicBarrier 是會被自動重置狀態的,從 reset 的方法實作中可以看出呼叫了 breakBarrier

方法,也就是說,呼叫 reset 會使當前處在等待中的執行緒最終拋出 BrokenBarrierException 并立即被喚醒,所以說 reset() 只會在你想打破屏障時才會使用

上述示例,我們構建 CyclicBarrier 物件時,并沒有傳遞 barrierCommand 物件, 我們修改示例傳入一個 barrierCommand 物件,看看會有什么結果:

// 創建 CyclicBarrier 實體,計數器的值設定為2
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
   log.info("全部運行結束");
});

運行結果:

從運行結果中來看,每次沖破屏障后都會執行 CyclicBarrier 初始化 barrierCommand 的方法, 這與我們對 doWait() 方法的分析完全吻合,從上面的運行結果中可以看出,最后一個執行緒是運行 barrierCommand run() 方法的執行緒,我們再來形象化的展示一下整個程序

從上圖可以看出,barrierAction 與每次突破屏障是串行化的執行程序,假如 barrierAction 是很耗時的匯總操作,那這就是可以優化的點了,我們繼續修改代碼

// 創建單執行緒執行緒池
private static Executor executor = Executors.newSingleThreadExecutor();

// 創建 CyclicBarrier 實體,計數器的值設定為2
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
   executor.execute(() -> gather());
});

private static void gather() {
   try {
      Thread.sleep(2000);
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
   log.info("全部運行結束");
}

我們這里將 CyclicBarrier 的回呼函式 barrierAction使用單執行緒的執行緒池,這樣最后一個沖破屏障的執行緒就不用等待 barrierAction 的執行,直接分配個執行緒池里的執行緒異步執行,進一步提升效率

運行結果如下:

我們再形象化的看一下整個程序:

這里使用了單一執行緒池,增加了并行操作,提高了程式運行效率,那問題來了:

如果 barrierAction 非常非常耗時,沖破屏障的任務就可能堆積在單一執行緒池的等待佇列中,就存在 OOM 的風險,那怎么辦呢?

這是就要需要一定的限流策略或者使用執行緒池的拒絕的略等

那把單一執行緒池換成非單一的固定執行緒池不就可以了嘛?比如 fixed(5)

乍一看確實能緩解單執行緒池可能引起的任務堆積問題,上面代碼我們看到的 gather() 方法,假如該方法內部沒有使用鎖或者說存在竟態條件,那 CyclicBarrier 的回呼函式 barrierAction 使用多執行緒必定引起結果的不準確

所以在實際使用中還要結合具體的業務場景不斷優化代碼,使之更加健壯

總結

本文講解了 CountDownLatch 和 CyclicBarrier 的經典使用場景以及實作原理,以及在使用程序中可能會遇到的問題,比如將大的 list 拆分作業就可以用到前者,讀取多個 Excel 的sheet 頁,最后進行結果匯總就可以用到后者 (文中完整示例代碼已上傳)

最后,再形象化的比喻一下

  • CountDownLatch 主要用來解決一個執行緒等待多個執行緒的場景,可以類比旅游團團長要等待所有游客到齊才能去下一個景點
  • 而 CyclicBarrier 是一組執行緒之間的相互等待,可以類比幾個驢友之間的不離不棄,共同到達某個地方,再繼續出發,這樣反復

靈魂追問

  1. 怎樣拿到 CyclicBarrier 的匯總結果呢?
  2. 執行緒池中的 Future 特性你有使用過嗎?

接下來,咱們就聊聊那些可以使用的 Future 特性

參考

  1. Java 并發編程實戰
  2. Java 并發編程的藝術
  3. Java 并發編程之美
  4. When to reset CyclicBarrier in java multithreading
    個人博客:https://dayarch.top
    加我微信好友, 進群娛樂學習交流,備注「進群」

歡迎持續關注公眾號:「日拱一兵」

  • 前沿 Java 技術干貨分享
  • 高效工具匯總 | 回復「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回復「資料」

以讀偵探小說思維輕松趣味學習 Java 技術堆疊相關知識,本著將復雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......


轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/153777.html

標籤:Java

上一篇:恕我直言你可能真的不會java第10篇-集合元素歸約

下一篇:Maven中央倉庫正式成為Oracle官方JDBC驅動程式組件分發中心

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more