本系列將梳理一下多線程同步的一些經常用到方法或類,包括有CountDownLatch,CyclicBarriar,join,synchronized,wait/notify/notifyAll,Semaphore,ReentrantLock,Phaser,Future,Exchanger,concurrent等。通過這些方法或類的對比和使用,不僅可以拓寬我們的知識面,提高我們的線程同步處理能力,還可以讓我們在面試時可以更從容和淡定。
本節將首先介紹一下CountDownLatch–倒計數鎖存器
CountDownLatch的一個特別典型的應用場景是:有一個任務想要繼續往下執行,它必須等到其它任務執行完畢後纔可以繼續往下執行。
1、CountDownLatch是什麼?
CountDownLatch是在Java1.5中被引入的一個線程同步類,跟它一起被引入的併發工具類還有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue等,它們都位在於java.util.concurrent包中。
CountDownLatch是通過一個計數器來實現的,計數器的初始值爲線程的數量。每當一個線程完成了自己的任務後,計數器的值就會減1。當計數器值到達0時,它表示所有的線程已經完成了任務,然後在閉鎖上等待的線程就可以恢復執行任務。
因此CountDownLatch這個類用作這樣的場景,一個線程在等待其他線程完成各自的工作後再執行。例如,應用程序的主線程希望一些初始化參數或者屬性的子線程都完成後再執行。
2、基本元素和常用方法
CountDownLatch類有4個基本元素或方法:
- 初始值。它決定CountDownLatch類需要等待的事件或者線程的數量。
- await() 方法。 被等待全部事件終結的線程調用。
- countDown() 。方法事件在結束執行後調用。
- await(long time, TimeUnit unit)。此方法會休眠直到被中斷;
當創建 CountDownLatch 對象時,對象使用構造函數的參數來初始化內部計數器。每次調用 countDown() 方法, CountDownLatch 對象內部計數器減一。當內部計數器達到0時, CountDownLatch 對象喚醒全部使用 await() 方法睡眠的線程們。
CountDownLatch是不可逆的,即不可能重新初始化或者修改CountDownLatch對象的內部計數器的值。一旦計數器的值初始後,唯一可以修改它的方法就是之前用的 countDown() 方法,也就是隻能創建CountDownLatch對象,並使其內部值減少,不能重新修改或增加內部值。當計數器到達0時, 全部調用 await() 方法將會立刻返回,後面再調用此CountDownLatch對象的countDown() 方法都將不會產生任何作用。後續會介紹CyclicBarriar,它除了可以實現CountDownLatch的功能外,還可以重新進行初始化,重複使用。
另一種版本的 await() 方法是await(long time, TimeUnit unit),表示CountDownLatch內部計數器到達0或者特定的時間過去了。TimeUnit 類包含了:DAYS, HOURS, MICROSECONDS, MILLISECONDS, MINUTES, NANOSECONDS和 SECONDS等多種類型,在此方法中都可以使用。
3、演示代碼
以下是使用了兩個CountDownLatch的demo, CountDownLatch begin保證了所有線程都能在相同時間點開始處理。CountDownLatch end保證所有線程執行完畢後,主線程再退出。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchDemo {
//賽跑運動員的個數
private static final int RUNNER_AMOUNT = 5;
public CountDownLatchDemo() {
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//對於每位運動員,裁判員發令槍響了後纔開始比賽,只有一次發令槍
CountDownLatch cdl_startgun = new CountDownLatch(1);
//對於整個比賽,所有賽跑人員跑完後纔算結束,有RUNNER_AMOUNT個運動員
CountDownLatch cdl_runner = new CountDownLatch(RUNNER_AMOUNT);
Runner[] plays = new Runner[RUNNER_AMOUNT];
for(int i=0;i<RUNNER_AMOUNT;i++)
plays[i] = new Runner(i+1,cdl_startgun,cdl_runner);
//定義特定的線程池,大小爲5,每個線程對應一個運動員
ExecutorService exe = Executors.newFixedThreadPool(RUNNER_AMOUNT);
for(Runner p:plays)
exe.execute(p); //分配線程
System.out.println("Race begins!");
//啓動所有運動員的線程後,發令槍發令,cdl_startgun歸0
cdl_startgun.countDown();
try{
//開始等待所有運動員結束各自的跑步,只有都跑完後才能繼續運行,否則一直處於阻塞狀態
cdl_runner.wait(); //等待cdl_runner變爲0,即爲比賽結束
}catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}finally{
System.out.println("Race ends!");
}
exe.shutdown();
}
}
接下來是Runner類:
import java.util.concurrent.CountDownLatch;
public class Runner implements Runnable {
private int id;
private CountDownLatch cdl_startgun;
private CountDownLatch cdl_runner;
public Runner(int i, CountDownLatch cdl_startgun, CountDownLatch cdl_runner) {
// TODO Auto-generated constructor stub
super();
this.id = i;
this.cdl_startgun = cdl_startgun;
this.cdl_runner = cdl_runner;
}
@Override
public void run() {
// TODO Auto-generated method stub
try{
//等待發令槍響起,一旦發令槍響過,cdl_startgun歸0,程序就可以往下執行
cdl_startgun.await(); //等待begin的狀態爲0
Thread.sleep((long)(Math.random()*100)); //隨機分配時間,即運動員完成時間
System.out.println("Play"+id+" arrived.");
}catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}finally{
//本運動員完成跑步,通知cdl_runner減1
cdl_runner.countDown(); //使end狀態減1,最終減至0
}
}
}
執行結果可能是這樣:
Race begins!
Play2 arrived.
Play4 arrived.
Play3 arrived.
Play5 arrived.
Play1 arrived.
Race ends!
歡迎您掃一掃上面的微信公衆號,訂閱我的個人公衆號!本公衆號將以推送Android各種碎片化小知識或小技巧,以及整理Android網絡,架構,面試等方面的知識點爲主,也會不定期將開發老司機日常工作中踩過的坑,平時自學的一些知識總結出來進行分享。每天一點乾貨小知識把你的碎片時間充分利用起來。