併發編程之倒計數閉鎖CountDownLatch

       CountDownLatch看作一個計數器,只不過這個計數器的操作是原子操作,同時只能有一個線程去操作這個計數器,也就是同時只能有一個線程去減這個計數器裏面的值。CountDownLatch的一個非常典型的應用場景是:有一個任務想要往下執行,但必須要等到其他的任務執行完畢後纔可以繼續往下執行。假如我們這個想要繼續往下執行的任務調用一個CountDownLatch對象的await()方法,其他的任務執行完自己的任務後調用同一個CountDownLatch對象上的countDown()方法,這個調用await()方法的任務將一直阻塞等待,直到這個CountDownLatch對象的計數值減到0爲止。 所以,CountDownLatch類可以用於控制多個線程同時開始運行,或者用於主線程等待所有子線程的某個執行結果。一句話就是它會在倒數結束之前像門閂一樣阻止線程的前進(就是阻塞)。它的用途和Thread.join()方法類似,都是用於線程間的協作。在一些場景,比如你需要用線程池去啓用多個線程併發地做一些處理,但是這之後的工作又必須要等這些併發的任務都完成之後再繼續。在這時你就可以使用CountDownLatch了。

     例:百米賽跑:8名運動員同時起跑,由於速度的快慢,肯定有會出現先到終點和晚到終點的情況,而終點有個統計成績的儀器,當所有選手到達終點時,它會統計所有人的成績並進行排序,然後把結果發送到彙報成績的系統。 其實這就是一個CountDownLatch的應用場景:一個線程或多個線程等待其他線程運行達到某一目標後進行自己的下一步工作,而被等待的“其他線程”達到這個目標後繼續自己下面的任務。實例中的EndCounter等待所有worker線程結束賽跑過程後將endSignal變爲0,在此之前EndCounter一直處於等待狀態,在beginSignal被EndCounter變爲0之前所有worker都處於就緒狀態的線程繼續處於等待狀態。總之,EndCounter將beginSignal變爲0之後,worker纔可以執行,這樣就可以使得所有worker同時執行他們的任務,而在worker將endSignal變爲0之前處於等待狀態,這使得EndCounter得到所有worker的執行結果之後再往下執行,但是當worker將endCounter減一之後可以繼續執行自己後面的任務。
     這個場景中:
     1. 被等待的“其他線程”------>8名運動員
     2. 等待“其他線程”的這個線程------>終點統計成績的儀器
     那麼,如何來通過CountDownLatch來實現上述場景的線程控制和調度呢?CountDownLatch類有一個常用的構造方法:CountDownLatch(int count); 兩個常用的方法:await()和countdown() ;其中count是一個計數器中的初始化數字,比如初始化的數字是8,當一個線程裏調用了countdown(),則這個計數器就減一,當線程調用了 await(),則這個線程就等待這個計數器變爲0,當這個計數器變爲0時,這個線程繼續自己下面的工作。程序源代碼如下所示:

Worker源代碼:

import java.util.concurrent.CountDownLatch;

public class Worker implements Runnable {
	private int workerID;
	private CountDownLatch beginSignal;
	private CountDownLatch endSignal;

	public Worker(int workerID, CountDownLatch begin, CountDownLatch end) {
		this.workerID = workerID;
		this.beginSignal = begin;
		this.endSignal = end;
	}

	@Override
	public void run() {
		try {
			// 所有work線程都在此處等待beginSingle變爲0,在beginSignal變爲0之前所有worker必須等待
			beginSignal.await();
			System.out.println("worker-" + workerID + "開始起跑...");
			System.out.println("worker-" + workerID + "到達終點");
			endSignal.countDown();// 將endSignal--
			System.out.println("worker-" + workerID + "繼續執行自己的任務");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
EndCounter源代碼:

import java.util.concurrent.CountDownLatch;

public class EndCounter {


	public static void main(String[] args) {
		//beginSignal用於主線程控制處理worker統一執行
		CountDownLatch beginSignal = new CountDownLatch(1);
		//endSignal用於主線程等待worker線程的執行結果,得到結果之後主線程繼續向下執行
		CountDownLatch endSignal = new CountDownLatch(8);


		for (int i = 0; i < 8; i++) {
			new Thread(new Worker(i + 1, beginSignal, endSignal)).start();
		}


		try {
			System.out.println("主面試官即將開始發佈起跑命令");
			beginSignal.countDown(); // begSignal變爲0,所有worker線程統一起跑
			endSignal.await(); // 等待運動員到達終點,即endSignal變爲0
			System.out.println("結果發送到彙報成績的系統");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
執行結果(其中一種情況):

主面試官即將開始發佈起跑命令
worker-1開始起跑...
worker-1到達終點
worker-1繼續執行自己的任務
worker-2開始起跑...
worker-2到達終點
worker-2繼續執行自己的任務
worker-5開始起跑...
worker-3開始起跑...
worker-6開始起跑...
worker-6到達終點
worker-6繼續執行自己的任務
worker-4開始起跑...
worker-4到達終點
worker-4繼續執行自己的任務
worker-8開始起跑...
worker-7開始起跑...
worker-7到達終點
worker-7繼續執行自己的任務
worker-3到達終點
worker-5到達終點
worker-3繼續執行自己的任務
worker-8到達終點
worker-5繼續執行自己的任務
結果發送到彙報成績的系統
worker-8繼續執行自己的任務
       關於CountDownLatch、CyclicBarrier、Semaphore的幾點說明:
      如果是一個線程等待一個線程,則可以通過await()和notify()來實現;如果是一個線程等待多個線程,則就可以使用CountDownLatch和CyclicBarrier來實現比較好的控制。 CyclicBarrier可以循環利用,也就是在到達屏障之後,線程就會被銷燬,而CountDownLatch則不會,各個線程還繼續執行自己的任務。CountDownLatch是等大家都跑完了再繼續,CyclicBarrier是等大家準備好了一起跑,Semaphore是隻允許一定數量的跑。






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