[ 本文主要從整體上介紹Java中的多線程技術,對於一些重要的基礎概念會進行相對詳細的介紹,若有敘述不清晰以及不合理的地方,希望大家指出,謝謝大家:) ]
一、為什么使用多線程
1. 並發與並行
2. 阻塞與非阻塞
typedef ssize_t int; typedef size_t unsigned; ssize_t read(int fd, void *buf, size_t n);
3. 多進程 vs 多線程
二、如何使用多線程
1. 線程執行模型
2. 創建一個新線程
(1)通過實現Runnable接口
class MyRunnable implements Runnable { ... public void run() { //這里是新線程需要執行的任務 } } Runnable r = new MyRunnable(); Thread t = new Thread(r);
(2)通過繼承Thread類
class MyThread extends Thread { public void run() { //這里是線程要執行的任務 } }
t.start();
(3)兩種方式的比較
既然有兩種方式可以創建線程,那么我們該使用哪一種呢?首先,直接繼承Thread類的方法看起來更加方便,但它存在一個局限性:由於Java中不允許多繼承,我們自定義的類繼承了Thread后便不能再繼承其他類,這在有些場景下會很不方便;實現Runnable接口的那個方法雖然稍微繁瑣些,但是它的優點在於自定義的類可以繼承其他的類。
3. 線程的屬性
(1)線程的狀態
- New(新生):線程對象剛剛被創建出來;
- Runnable(可運行):在線程對象上調用start方法后,相應線程便會進入Runnable狀態,若被線程調度程序調度,這個線程便會成為當前運行(Running)的線程;
- Blocked(被阻塞):若一段代碼被線程A”上鎖“,此時線程B嘗試執行這段代碼,線程B就會進入Blocked狀態;
- Waiting(等待):當線程等待另一個線程通知線程調度器一個條件時,它本身就會進入Waiting狀態;
- Time Waiting(計時等待):計時等待與等待的區別是,線程只等待一定的時間,若超時則不再等待;
- Terminated(被終止):線程的run方法執行完畢或者由於一個未捕獲的異常導致run方法意外終止會進入Terminated狀態。
后文中若不加特殊說明的話,我們會用阻塞狀態統一指代Blocked、Waiting、Time Waiting。
(2)線程的優先級
void setPriority(int newPriority) //設置線程的優先級,可以使用系統提供的三個優先級常量 static void yield() //使當前線程處於讓步狀態,這樣當存在其他優先級大於等於本線程的線程時,線程調度程序會調用那個線程
4. Thread類
Thread實現了Runnable接口,關於這個類的以下實例域需要我們了解:
private volatile char name[]; //當前線程的名字,可在構造器中指定 private int priority; //當前線程優先級 private Runnable target; //當前要執行的任務 private long tid; //當前線程的ID
Thread類的常用方法除了我們之前提到的用於啟動線程的start外還有:
- sleep方法,這是一個靜態方法,作用是讓當前線程進入休眠狀態(但線程不會釋放已獲取的鎖),這個休眠狀態其實就是我們上面提到過的Time Waiting狀態,從休眠狀態“蘇醒”后,線程會進入到Runnable狀態。sleep方法有兩個重載版本,聲明分別如下:
public static native void sleep(long millis) throws InterruptedException; //讓當前線程休眠millis指定的毫秒數 public static native void sleep(long millis, int nanos) throws InterruptedException; //在毫秒數的基礎上還指定了納秒數,控制粒度更加精細
- join方法,這是一個實例方法,在當前線程中對一個線程對象調用join方法會導致當前線程停止運行,等那個線程運行完畢后再接着運行當前線程。也就是說,把當前線程還沒執行的部分“接到”另一個線程后面去,另一個線程運行完畢后,當前線程再接着運行。join方法有以下重載版本:
public final synchronized void join() throws InterruptedException public final synchronized void join(long millis) throws InterruptedException; public final synchronized void join(long millis, int nanos) throws InterruptedException;
無參數的join表示當前線程一直等到另一個線程運行完畢,這種情況下當前線程會處於Wating狀態;帶參數的表示當前線程只等待指定的時間,這種情況下當前線程會處於Time Waiting狀態。當前線程通過調用join方法進入Time Waiting或Waiting狀態后,會釋放已經獲取的鎖。實際上,join方法內部調用了Object類的實例方法wait,關於這個方法我們下面會具體介紹。
- yield方法,這是一個靜態方法,作用是讓當前線程“讓步”,目的是為了讓優先級不低於當前線程的線程有機會運行,這個方法不會釋放鎖。
- interrupt方法,這是一個實例方法。每個線程都有一個中斷狀態標識,這個方法的作用就是將相應線程的中斷狀態標記為true,這樣相應的線程調用isInterrupted方法就會返回true。通過使用這個方法,能夠終止那些通過調用可中斷方法進入阻塞狀態的線程。常見的可中斷方法有sleep、wait、join,這些方法的內部實現會時不時的檢查當前線程的中斷狀態,若為true會立刻拋出一個InterruptedException異常,從而終止當前線程。
以下這幅圖很好的詮釋了隨着各種方法的調用,線程在不同的狀態之間的切換(圖片來源:http://www.cnblogs.com/dolphin0520/p/3920357.html):
5. wait方法與notify/notifyAll方法
(1)wait方法
wait方法是Object類中定義的實例方法。在指定對象上調用wait方法能夠讓當前線程進入阻塞狀態(前提時當前線程持有該對象的內部鎖(monitor)),此時當前線程會釋放已經獲取的那個對象的內部鎖,這樣一來其他線程就可以獲取這個對象的內部鎖了。當其他線程獲取了這個對象的內部鎖,進行了一些操作后可以調用notify方法來喚醒正在等待該對象的線程。
(2)notify/notifyAll方法
notify/notifyAll方法也是Object類中定義的實例方法。它倆的作用是喚醒正在等待相應對象的線程,區別在於前者喚醒一個等待該對象的線程,而后者喚醒所有等待該對象的線程。這么說比較抽象,下面我們來舉一個具體的例子來說明以下wait和notify/notifyAll的用法。請看以下代碼(轉自Java並發編程:線程間協作的兩種方式):
1 public class Test { 2 private int queueSize = 10; 3 private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize); 4 5 public static void main(String[] args) { 6 Test test = new Test(); 7 Producer producer = test.new Producer(); 8 Consumer consumer = test.new Consumer(); 9 10 producer.start(); 11 consumer.start(); 12 } 13 14 class Consumer extends Thread{ 15 16 @Override 17 public void run() { 18 consume(); 19 } 20 21 private void consume() { 22 while(true){ 23 synchronized (queue) { 24 while(queue.size() == 0){ 25 try { 26 System.out.println("隊列空,等待數據"); 27 queue.wait(); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 queue.notify(); 31 } 32 } 33 queue.poll(); //每次移走隊首元素 34 queue.notify(); 35 System.out.println("從隊列取走一個元素,隊列剩余"+queue.size()+"個元素"); 36 } 37 } 38 } 39 } 40 41 class Producer extends Thread{ 42 43 @Override 44 public void run() { 45 produce(); 46 } 47 48 private void produce() { 49 while(true){ 50 synchronized (queue) { 51 while(queue.size() == queueSize){ 52 try { 53 System.out.println("隊列滿,等待有空余空間"); 54 queue.wait(); 55 } catch (InterruptedException e) { 56 e.printStackTrace(); 57 queue.notify(); 58 } 59 } 60 queue.offer(1); //每次插入一個元素 61 queue.notify(); 62 System.out.println("向隊列取中插入一個元素,隊列剩余空間:"+(queueSize-queue.size())); 63 } 64 } 65 } 66 } 67 }
以上代碼描述的是經典的“生產者-消費者”問題。Consumer類代表消費者,Producer類代表生產者。在生產者進行生產之前(對應第48行的produce方法),會獲取queue的內部鎖(monitor)。然后判斷隊列是否已滿,若滿了則無法再生產,所以在第54行調用queue.wait方法,從而等待在queue對象上。(釋放了queue的內部鎖)此時生產者能夠能夠獲取queue的monitor從而進入第21行的consume方法,這樣一來它就會通過第33行的queue.poll方法進行消費,於是隊列不再滿了,接着它在第34行調用queue.notify方法來通知正在等待的生產者,生產者就會從剛才阻塞的wait方法(第54行)中返回。
同理,當隊列空時,消費者也會等待(第27行)生產者來喚醒(第61行)。
await方法和signal/signalAll方法是wait方法和notify/notifyAll方法的升級版,在后文中會具體介紹它們與wait、notify/notifyAll之間的關系。
6. 如何保證線程安全
所謂線程安全,指的是當多個線程並發訪問數據對象時,不會造成對數據對象的“破壞”。保證線程安全的一個基本思路就是讓訪問同一個數據對象的多個線程進行“排隊”,一個接一個的來,這樣就不會對數據造成破壞,但帶來的代價是降低了並發性。
(1)race condition(竟爭條件)
public class Counter { private long count = 0; public void add(long value) { this.count = this.count + value; } }
我們注意一下改變count值的那一行,通常這個操作不是一步完成的,它大概分為以下三步:
- 第一步,把count的值加載到寄存器中;
- 第二步,把相應寄存器的值加上value的值;
- 第三步,把寄存器的值寫回count變量。

(2)鎖對象
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
我們來分別介紹下Lock接口中發方法:
- lock方法用來獲取鎖,在鎖被占用時它會一直阻塞,並且這個方法不能被中斷;
- lockInterruptibly方法在獲取不到鎖時也會阻塞,它與lock方法的區別在於阻塞在該方法時可以被中斷;
- tryLock方法也是用來獲取鎖的,它的無參版本在獲取不到鎖時會立刻返回false,它的計時等待版本會在等待指定時間還獲取不到鎖時返回false,計時等待的tryLock在阻塞期間也能夠被中斷。使用tryLock方法的典型代碼如下:
if (myLock.tryLock()) { try { … } finally { myLock.unlock(); } } else { //做其他的工作 }
- unlock方法用來釋放鎖;
- newCondition方法用來獲取當前鎖對象相關的條件對象,這個在下文我們會具體介紹。
Lock myLock = new ReentrantLock(); public void add(long value) { myLock.lock(); try { this.count = this.count + value; } finally { myLock.unlock(); } }
從以上代碼可以看到,使用ReentrantLock對象來上鎖時只需要先獲取一個它的實例。然后通過lock方法進行上鎖,通過unlock方法進行解鎖。注意,我們使用了一個try-finally塊,以確保即使發生異常也總是會解鎖,不然其他線程會一直無法執行add方法。當一個線程執行完“myLock.lock()”時,它就獲得了一個鎖對象,這就相當於它給臨界區上了鎖,其他線程都無法進來,只有這個線程執行完“myLock.unlock()"時,釋放了鎖對象,其他線程才能再通過“myLock.lock()"獲得鎖對象,從而進入臨界區。也就是說,當一個線程獲取了鎖對象后,其他嘗試獲取鎖對象的線程都會被阻塞,進入Blocked狀態,直至獲取鎖對象的線程釋放了鎖對象。
(3)條件對象
public void add(int value) { if (this.count > 5) { this.count = this.count + value; } }
public void add(int value) { myLock.lock(); try { while (counter.getCount() <= 5) { //等待直到大於5 } this.count = this.count + value; } finally { myLock.unlock(); } }
Condition enoughCount = myLock.newCondition();
(4)synchronized關鍵字
public synchronized void add(int value) { ... }
public void add(int value) { this.innerLock.lock(); try { ... } finally { this.innerLock.unlock(); } }
public synchronized void add(int value) { while (this.count <= 5) { wait(); } this.count += value; notifyAll(); }
這份代碼顯然比我們上面的實現要簡潔得多,實際開發中也更加常用。
- 不能中斷一個正在試圖獲取鎖的線程;
- 試圖獲取鎖時不能設定超時;
- 每個鎖僅有一個相關條件;
- 若我們需要多個線程進行讀操作,應該使用實現了Lock接口的ReentrantReadWriteLock類,這個類允許多個線程同時讀一個數據對象(這個類的使用后面會介紹);
- 當我們需要Lock/Condition的特性時,應該考慮使用它(比如多個條件還有計時等待版本的await函數);
- 一般場景我們可以考慮使用synchronized關鍵字,因為它的簡潔性一定程度上能夠減少出錯的可能。關於synchronized關鍵字需要注意的一點是:synchronized方法或者synchronized代碼塊出現異常時,Java虛擬機會自動釋放當前線程已獲取的鎖。
(5)同步阻塞
synchronized (obj) { //臨界區 }
一個線程執行上面的代碼塊便可以獲取obj對象的內部鎖,直至它離開這個代碼塊才會釋放鎖。
public class Counter { private Object lock = new Object(); synchronized (lock) { //臨界區 } ... }
那么這種使用這種鎖有什么好處呢?我們知道Counter對象只有一個內部鎖,這個內部鎖在同一時刻只能被一個對象持有,那么設想Counter對象中定義了兩個synchronized方法。在某一時刻,線程A進入了其中一個synchronized方法並獲取了內部鎖,此時線程B嘗試進去另一個synchronized方法時由於對象內部鎖還沒有被線程A釋放,因此線程B只能被阻塞。然而我們的兩個synchronized方法是兩個不同的臨界區,它們不會相互影響,所以它們可以在同一時刻被不同的線程所執行。這時我們就可以使用如上面所示的顯式的鎖對象,它允許不同的方法同步在不同的鎖上。
(6)volatile域
(7)死鎖
假設現在進程中只有線程A和線程B這兩個線程,考慮下面這樣一種情形:
線程A獲取了counterA對象的內部鎖,線程B獲取了counterB對象的內部鎖。而線程A只有在獲取counterB的內部鎖后才能繼續執行,線程B只有在獲取線程A的內部鎖后才能繼續執行。這樣一來,兩個線程在互相等待對方釋放鎖從而誰也沒法繼續執行,這種現象就叫做死鎖(deadlock)。
除了以上情況,還有一種類似的死鎖情況是兩個線程獲取鎖后都不滿足條件從而進入條件的等待集中,相互等待對方喚醒自己。
Java沒有為解決死鎖提供內在機制,因此我們只有在開發時格外小心,以避免死鎖的發生。關於分析定位程序中的死鎖,大家可以參考這篇文章:Java Deadlock Example and How to analyze deadlock situation
(8)讀/寫鎖
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
我們可以看到這個接口就定義了兩個方法,其中readLock方法用來獲取一個“讀鎖”,writeLock方法用來獲取一個“寫鎖”。
ReentrantReadWriteLock類的使用步驟通常如下所示://構造一個ReentrantReadWriteLock對象 private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //分別從中“提取”讀鎖和寫鎖 private Lock readLock = rwl.readLock(); private Lock writeLock = rwl.writeLock(); //對所有的Reader線程加讀鎖 readLock.lock(); try { //讀操作可並發,但寫操作會互斥 } finally { readLock.unlock(); } //對所有的Writer線程加寫鎖 writeLock.lock(); try { //排斥所有其他線程的讀和寫操作 } finally { writeLock.unlock(); }
在使用ReentrantReadWriteLock類時,我們需要注意以下兩點:
- 若當前已經有線程占用了讀鎖,其他要申請寫鎖的線程需要占用讀鎖的線程釋放了讀鎖才能申請成功;
- 若當前已經有線程占用了寫鎖,其他要申請讀鎖或寫鎖的線程都需要等待占用寫鎖的線程釋放了寫鎖才能申請成功。
7. 阻塞隊列
- add方法:添加一個元素。若隊列已滿,會拋出IllegalStateException異常。
- element方法:返回隊列的頭元素。若隊列為空,會拋出NoSuchElementException異常。
- offer方法:添加一個元素,若成功則返回true。若隊列已滿,則返回false。
- peek方法:返回隊列的頭元素。若隊列為空,則返回null。
- poll方法:刪除並返回隊列的頭元素。若隊列為空,則返回null。
- put方法:添加一個元素。若隊列已滿,則阻塞。
- remove方法:移除並返回頭元素。若隊列為空,會拋出NoSuchElementException。
- take方法:移除並返回頭元素。若隊列為空,則阻塞。
- LinkedBlockingQueue是一個基於鏈表實現的阻塞隊列。默認容量沒有上限,但也有可以指定最大容量的構造方法。它有的“雙端隊列版本”為LinkedBlockingDeque。
- ArrayBlockingQueue是一個基於數組實現的阻塞隊列,它在構造時需要指定容量。它還有一個構造方法可以指定一個公平性參數,若這個參數為true,那么等待了最長時間的線程會得到優先處理(指定公平性參數會降低性能)。
- PriorityBlockingQueue是一個基於堆實現的帶優先級的阻塞隊列。元素會按照它們的優先級被移除隊列。
public class BlockingQueueTest { private int size = 20; private ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(size); public static void main(String[] args) { BlockingQueueTest test = new BlockingQueueTest(); Producer producer = test.new Producer(); Consumer consumer = test.new Consumer(); producer.start(); consumer.start(); } class Consumer extends Thread{ @Override public void run() { while(true){ try { //從阻塞隊列中取出一個元素 queue.take(); System.out.println("隊列剩余" + queue.size() + "個元素"); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread{ @Override public void run() { while (true) { try { //向阻塞隊列中插入一個元素 queue.put(1); System.out.println("隊列剩余空間:" + (size - queue.size())); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
8. 執行器
ExecutorService newCachedThreadPool() //返回一個帶緩存的線程池,該池在必要的時候創建線程,在線程空閑60s后終止線程 ExecutorService newFixedThreadPool(int threads) //返回一個線程池,線程數目由threads參數指明 ExecutorService newSingleThreadExecutor() //返回只含一個線程的線程池,它在一個單一的線程中依次執行各個任務
- 對於newCachedThreadPool方法返回的線程池:對每個任務,若有空閑線程可用,則立即讓它執行任務;若沒有可用的空閑線程,它就會創建一個新線程並加入線程池中;
- newFixedThreadPool方法返回的線程池里的線程數目由創建時指定,並一直保持不變。若提交給它的任務多於線程池中的空閑線程數目,那么就會把任務放到隊列中,當其他任務執行完畢后再來執行它們;
- newSingleThreadExecutor會返回一個大小為1的線程池,由一個線程執行提交的任務。
Future<T> submit(Callable<T> task) Future<T> submit(Runnable task, T result) Future<?> submit(Runnable task)
- 調用Executors中相關方法構建一個線程池;
- 調用submit方法提交一個Runnable對象或Callable對象到線程池中;
- 若想要取消一個任務,需要保存submit返回的Future對象;
- 當不再提交任何任務時,調用shutdown方法。
關於線程池更加深入及詳細的分析,大家可以參考這篇博文:http://www.cnblogs.com/dolphin0520/p/3932921.html
ScheduledFuture<V> schedule(Callable<V> task, long time, TimeUnit unit) ScheduledFuture<?> schedule(Runnable task, long time, TimeUnit unit) //以上兩個方法預定在指定時間過后執行任務 SchedukedFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) //在指定的延遲(initialDelay)過后,周期性地執行給定任務 ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit) //在指定延遲(initialDelay)過后周期性的執行任務,每兩個任務間的間隔為delay指定的時間
T invokeAny(Collection<Callable<T>> tasks) T invokeAny(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)
List<Future<T>> invokeAll(Collection<Callable<T>> tasks) List<Future<T>> invokeAll(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)
9. Callable與Future
public interface Callable<V> { V call() throws Exception; }
類型參數V即為異步方法call的返回值類型。
public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
- cancel方法用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。參數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設置true,則表示可以取消正在執行過程中的任務。如果任務已經完成,則無論mayInterruptIfRunning為true還是false,此方法肯定返回false(即如果取消已經完成的任務會返回false);如果任務正在執行,若mayInterruptIfRunning設置為true,則返回true,若mayInterruptIfRunning設置為false,則返回false;如果任務還沒有執行,則無論mayInterruptIfRunning為true還是false,肯定返回true。
- isCancelled方法表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。
- isDone方法表示任務是否已經完成,若任務完成,則返回true;
- get()方法用來獲取執行結果,這個方法會阻塞,一直等到任務執行完才返回;
- get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。
也就是說Future提供了三種功能:
- 判斷任務是否完成;
- 能夠中斷任務;
- 能夠獲取任務執行結果。
Future接口的實現類是FutureTask:
public class FutureTask<V> implements RunnableFuture<V>
FutureTask類實現了RunnableFuture接口,這個接口的定義如下:
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
FutureTask類有如下兩個構造器:
public FutureTask(Callable<V> callable) public FutureTask(Runnable runnable, V result)
FutureTask通常與線程池配合使用,通常會創建一個包裝了Callable對象的FutureTask實例,並用submit方法將它提交到一個線程池去執行,我們可以通過FutureTask的get方法獲取返回結果。
10. 同步容器與並發容器
(1)同步容器
Java中的同步容器指的是線程安全的集合類,同步容器主要包含以下兩類:
- 通過Collections類中的相應方法把普通容器類包裝成線程安全的版本;
- Vector、HashTable等系統為我們封裝好的線程安全的集合類。
相比與並發容器(下面會介紹),同步容器存在以下缺點:
- 對於並發讀訪問的支持不夠好;
- 由於內部多采用synchronized關鍵字實現,所以性能上不如並發容器;
- 對同步容器進行迭代的同時修改它的內容,會報ConcurrentModificationException異常。
關於同步容器更加詳細的介紹請參考這里:http://www.cnblogs.com/dolphin0520/p/3933404.html
(2)並發容器
並發容器相比於同步容器,具有更強的並發訪問支持,主要體現在以下方面:
- 在迭代並發容器時修改其內容並不會拋出ConcurrentModificationException異常;
- 在並發容器的內部實現中盡量避免了使用synchronized關鍵字,從而增強了並發性。
Java在java.util.concurrent包中提供了主要以下並發容器類:
- ConcurrentHashMap,這個並發容器是為了取代同步的HashMap;
- CopyOnWriteArrayList,使用這個類在迭代時進行修改不拋異常;
- ConcurrentLinkedQuerue是一個非阻塞隊列;
- ConcurrentSkipListMap用於在並發環境下替代SortedMap;
- ConcurrentSkipSetMap用於在並發環境下替代SortedSet。
關於這些類的具體使用,大家可以參考官方文檔及相關博文。通常來說,並發容器的內部實現做到了並發讀取不用加鎖,並發寫時加鎖的粒度盡可能小。
11. 同步器(Synchronizer)
java.util.concurrent包提供了幾個幫助我們管理相互合作的線程集的類,這些類的主要功能和適用場景如下:
- CyclicBarrier:它允許線程集等待直至其中預定數目的線程到達某個狀態(這個狀態叫公共障柵(barrier)),然后可以選擇執行一個處理障柵的動作。適用場景:當多個線程都完成某操作,這些線程才能繼續執行時,或都完成了某操作后才能執行指定任務時。對CyclicBarrier對象調用await方法即可讓相應線程進入barrier狀態,等到預定數目的線程都進入了barrier狀態后,這些線程就可以繼續往下執行了
- CountDownLatch:允許線程集等待直到計數器減為0。適用場景:當一個或多個線程需要等待直到指定數目的事件發生。舉例來說,假如主線程需要等待N個子線程執行完畢才繼續執行,就可以使用CountDownLatch來實現,需要用到CountDownLatch的以下方法:
1 public void await() throws InterruptedException { }; //調用該方法的線程會進入阻塞狀態,直到count值為0才繼續執行 2 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //await方法的計時等待版本 3 public void countDown() { }; //將CountDownLatch對象count值(初始化時作為參數傳入構造方法)減1
- Exchanger:允許兩個線程在要交換的對象准備好時交換對象。適用場景:當兩個線程工作在統一數據結構的兩個實例上時,一個向實例中添加數據,另一個從實例中移除數據。
- Semaphore:允許線程集等待直到被允許繼續運行為止。適用場景:限制同一時刻對某一資源並發訪問的線程數,初始化Semaphore需要指定許可的數目,線程要訪問受限資源時需要獲取一個許可,當所有許可都被獲取,其他線程就只有等待許可被釋放后才能獲取。
- SynchronousQueue:允許一個線程把對象交給另一個線程。適用場景:在沒有顯式同步的情況下,當兩個線程准備好將一個對象從一個線程傳遞到另一個線程。
關於CountDownLatch、CyclicBarrier、Semaphore的具體介紹和使用示例大家可以參考這篇博文:Java並發編程:CountDownLatch、CyclicBarrier和Semaphore。
三、參考資料
- Java並發編程:Callable、Future和FutureTask
- Java並發編程:阻塞隊列
- 《Java核心技術(卷一)》
- 《深入理解計算機系統(第二版)》
- Java並發編程:Lock