CountdownLatch
,CyclicBarrier
分別適合什麼場景呢?
大部分情況下, 子線程只需要關心自身執行的任務. 但在某些複雜的情況下, 需要使用多個線程來協同完成某個任務, 這就涉及到線程間通信(inter-thread communication)的問題了.
主要涉及的內容有:
thread.join()
方法object.wait()
方法object.notify()
方法CountdownLatch
類CyclicBarrier
類FutureTask
類Callable
類
示例代碼可參考: https://github.com/wingjay/HelloJava/blob/master/multi-thread/src/ForArticle.java
本文通過示例, 講解Java語言中, 如何實現線程間通信.
- 如何讓兩個線程順序執行
- 怎樣讓兩個線程交替執行
- 假設有四個線程: A,B,C,D, 如何實現ABC一起執行, 全部完成後再執行D線程.
- 短跑比賽, 在所有人都準備完成後, 讓運動員們同時起跑.
- 子線程執行完成後, 怎麼將結果值返回給主線程.
如何讓兩個線程順序執行
先看看基礎方法 printNumber(String)
的實現, 該方法按順序打印三個數字 1、2、3:
private static void printNumber(String threadName) {
int i=0;
while (i++ < 3) {
try {
Thread.sleep(100); // 注意這裏加入了延遲時間
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + "print:" + i);
}
}
假設有兩個線程: 線程A和線程B. 代碼如下:
/**
* A、B線程啓動順序是隨機的, 可多次執行來驗證
*/
private static void demo1() {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
printNumber("A");
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
printNumber("B");
}
});
A.start();
B.start();
}
每個線程都會調用 printNumber()
方法.
AB的執行順序隨機, 結果可能是這樣:
B print: 1
A print: 1
B print: 2
A print: 2
A print: 3
B print: 3
可以看到, A和B會一起執行.
假設需求發生變化, 線程A打印完成之後, 線程B才能執行打印. 那麼可以使用 thread.join()
方法,代碼如下:
/**
* 打印順序: A 1, A 2, A 3, B 1, B 2, B 3
*/
private static void demo2() {
final Thread A = new Thread(new Runnable() {
@Override
public void run() {
printNumber("A");
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("B線程需要等待A線程執行完成");
try {
A.join(); // 等待線程A執行完成之後與當前線程“匯合”
} catch (InterruptedException e) {
e.printStackTrace();
}
printNumber("B");
}
});
A.start();
B.start();
}
join, 加入, 合併, 匯合
執行結果爲:
B線程需要等待A線程執行完成
A print: 1
A print: 2
A print: 3
B print: 1
B print: 2
B print: 3
可以看到, B線程執行的方法裏面, 調用了 A.join()
方法, 會等待A線程先執行完成, B再繼續往下走.
怎樣讓兩個線程以指定順序交替執行
假設需要先讓A打印1、然後B打印1,2,3, 再讓A打印2、3. 那麼, 可以使用細粒度的鎖(fine-grained locks)來控制執行順序.
比如使用Java內置的 object.wait()
和 object.notify()
方法. 代碼如下:
/**
* 打印順序: A 1, B 1, B 2, B 3, A 2, A 3
*/
private static void demo3() {
final Object lock = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("A 1");
try {
System.out.println("A waiting…");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A 2");
System.out.println("A 3");
}
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("B 1");
System.out.println("B 2");
System.out.println("B 3");
lock.notify();
}
}
});
A.start();
//
try {
TimeUnit.MILLISECONDS.sleep(50L);
} catch (InterruptedException e) {
e.printStackTrace();
}
B.start();
}
執行結果如下:
A 1
A waiting…
B 1
B 2
B 3
A 2
A 3
這就實現了需要的效果.
主流程說明
- 首先創建一個對象鎖:
lock = new Object();
- A獲取鎖, 得到後先打印1, 然後調用
lock.wait()
進入等待狀態, 同時移交鎖的控制權;- B暫時不會執行打印, 需要等A調用
lock.wait()
釋放鎖之後, B 獲得鎖纔開始執行打印;- B打印出1、2、3, 然後調用
lock.notify()
方法來喚醒等待這個鎖的線程(A);- A被喚醒之後, 等待獲得鎖, 之後繼續打印剩下的2和3.
下面加上一些日誌, 來幫助我們理解這段代碼.
/**
* demo3的基礎上-加日誌
* 打印順序: A 1, B 1, B 2, B 3, A 2, A 3
*/
private static void demo4() {
final Object lock = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("====提示: A 等待鎖...");
synchronized (lock) {
System.out.println("====提示: A 得到了鎖 lock");
System.out.println("A 1");
try {
System.out.println("====提示: A 調用lock.wait()放棄鎖的控制權,並等待...");
lock.wait();
System.out.println("====提示: A在lock.wait()之後,再次獲得鎖的控制權,HAHAHA");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("====提示: A線程被喚醒, A 重新獲得鎖 lock");
System.out.println("A 2");
System.out.println("A 3");
}
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("====提示: B 等待鎖...");
synchronized (lock) {
System.out.println("====提示: B 得到了鎖 lock");
System.out.println("B 1");
System.out.println("B 2");
System.out.println("B 3");
System.out.println("====提示: B 打印完畢, 調用 lock.notify() 方法");
lock.notify();
// 看看A能不能獲得鎖
try {
System.out.println("====提示: B 調用 lock.notify()完成,睡10秒看看...");
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("====提示: B 調用 lock.notify()完成,退出synchronized塊");
}
}
});
A.start();
//
try {
TimeUnit.MILLISECONDS.sleep(50L);
} catch (InterruptedException e) {
e.printStackTrace();
}
//
B.start();
}
在其中, 我們加入了一些調皮的邏輯, 執行結果如下:
====提示: A 等待鎖...
====提示: A 得到了鎖 lock
A 1
====提示: A 調用lock.wait()放棄鎖的控制權,並等待...
====提示: B 等待鎖...
====提示: B 得到了鎖 lock
B 1
B 2
B 3
====提示: B 打印完畢, 調用 lock.notify() 方法
====提示: B 調用 lock.notify()完成,睡10秒看看...
====提示: B 調用 lock.notify()完成,退出synchronized塊
====提示: A在lock.wait()之後,再次獲得鎖的控制權,HAHAHA
====提示: A線程被喚醒, A 重新獲得鎖 lock
A 2
A 3
可以看到, 雖然B調用了
lock.notify()
方法喚醒了某個等待的線程(A), 但因爲同步代碼塊還未執行完, 所以沒有釋放這個鎖; 直到睡了10秒鐘, 繼續執行後面的代碼, 退出同步代碼塊之後, A 才獲得執行機會.
- synchronized代碼塊結束, 會自動釋放鎖.
Object#wait()
則是臨時放棄鎖, 進入沉睡, 必須有人喚醒(notify), 否則會睡死, 或者被超時打斷.Object#notify()
只是喚醒某個線程, 並沒有釋放鎖.
ABC全部執行完成後再執行D
前面介紹的 thread.join()
方法, 等待另一個線程(thread)運行完成後, 當前線程才執行(: 等TA忙完了來匯合). 但如果我們使用 join 方法來等待A、B和C的話, 它將使A,B,C依次執行, 但我們希望的是他們仨同步運行.
想要達成的目標是: A,B,C 三個線程同時運行, 每個線程完成後, 通知D一聲; 等A,B,C都運行完成, D纔開始運行. 我們可以使用CountdownLatch
來實現這種類型的通信. 其基本用法爲:
- 創建一個計數器(counter), 並設置初始值:
CountdownLatch countDownLatch = new CountDownLatch(3);
- 需要等待的線程, 調用
countDownLatch.await()
方法進入等待狀態, 直到 count 值變成0爲止; - 其他線程調用
countDownLatch.countDown()
來將 count 值減小; - 當其他線程調用
countDown()
將 count 值減小爲0, 等待線程中的countDownLatch.await()
方法將立即返回, 那麼這個線程也就可以繼續執行後續的代碼.
實現代碼如下:
private static void runDAfterABC() {
int worker = 3;
final CountDownLatch countDownLatch = new CountDownLatch(worker);
Thread D = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("D 線程即將調用 countDownLatch.await(); 等待其他線程通知. ");
try {
countDownLatch.await();
System.out.println("其他線程全部執行完成, D 開始幹活...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
D.start();
//
for (char threadName='A'; threadName <= 'C'; threadName++) {
final String tN = String.valueOf(threadName);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(tN + " 線程正在執行...");
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(tN + " 線程執行完畢, 調用 countDownLatch.countDown()");
countDownLatch.countDown();
}
}).start();
}
}
結果如下:
D 線程即將調用 countDownLatch.await(); 等待其他線程通知.
A 線程正在執行...
B 線程正在執行...
C 線程正在執行...
B 線程執行完畢, 調用 countDownLatch.countDown()
C 線程執行完畢, 調用 countDownLatch.countDown()
A 線程執行完畢, 調用 countDownLatch.countDown()
其他線程全部執行完成, D 開始幹活...
事實上, CountDownLatch
本身是一個倒數計數器, 我們將初始值設置爲3. 當D運行時, 首先調用 countDownLatch.await()
方法檢查 counter 值是否爲0, 如果counter值不是則會等待. A、B和C線程在自身運行完成後, 通過 countDownLatch.countDown()
方法將 counter 值減1. 當3個線程都執行完, A, B, C將 counter 值將會減小到0; 然後,D線程中的 await()
方法就會返回, D線程將繼續執行.
因此, CountDownLatch
適用於一個線程等待多個線程的場景.
運動員同時起跑的問題
假設3個運動員都確定做好預備, 然後同時起跑.
用3個線程來模擬, A,B,C線程各自準備, 等全部準備就緒, 同時開始運行. 如何用代碼來實現呢?
前面介紹的CountDownLatch
可以用來計數, 但計數完成後, 只會有一個線程的 await()
方法得到響應, 所以不太適合多個線程同時等待的情況.
要達到線程互相等待的效果, 可以使用 CyclicBarrier
, 其基本用法爲:
- 首先創建一個公開的
CyclicBarrier
對象, 並設置同時等待的線程數量,CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
- 這些線程各自進行準備, 自身準備好之後, 還需要等其他線程準備完畢, 即調用
cyclicBarrier.await()
方法來等待; - 當需要同時等待的線程全部調用了
cyclicBarrier.await()
方法, 也就意味着這些線程都準備好了, 那麼這些線程就可以繼續執行.
注意是
Cyclic
, 不是 Cycle.
實現代碼如下. 假設有三個運動員同時開始賽跑, 每個人都需要等其他人準備就緒.
private static void runABCWhenAllReady() {
int runner = 3;
CyclicBarrier cyclicBarrier = new CyclicBarrier(runner);
final Random random = new Random();
for (char runnerName='A'; runnerName <= 'C'; runnerName++) {
final String rN = String.valueOf(runnerName);
new Thread(new Runnable() {
@Override
public void run() {
long prepareTime = random.nextInt(10000) + 100;
System.out.println(rN + " 需要的準備時間:" + prepareTime);
try {
Thread.sleep(prepareTime);
} catch (Exception e) {
e.printStackTrace();
}
try {
System.out.println(rN + " 準備完畢, 等其他人... ");
cyclicBarrier.await(); // 當前線程準備就緒, 等待其他人的反饋
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(rN + " 開始跑動~加速~"); // 所有線程一起開始
}
}).start();
}
}
結果如下:
A 需要的準備時間: 4131
B 需要的準備時間: 6349
C 需要的準備時間: 8206
A 準備完畢, 等其他人...
B 準備完畢, 等其他人...
C 準備完畢, 等其他人...
C 開始跑動~加速~
A 開始跑動~加速~
B 開始跑動~加速~
將子線程的執行結果返回給主線程
當然, 也有簡單的辦法, 比如使用
ConcurrentHashMap
在實際開發中, 經常需要使用新線程來執行一些耗時任務, 然後將執行結果返回給主線程.
一般情況下, 創建新線程時, 我們會將Runnable對象傳給線程來執行. Runnable接口的定義如下:
public interface Runnable {
public abstract void run();
}
run()
方法不返回任何結果. 那麼如果想要獲取返回結果時怎麼辦呢? 我們可以使用一個類似的接口: Callable
:
@FunctionalInterface
public interface Callable<V> {
/**
* 返回執行結果, 如果出錯則可以拋出異常.
*
* @return 執行結果(computed result)
* @throws Exception, 如果不能計算出結果
*/
V call() throws Exception;
}
可以看出, Callable
最大的區別在於返回泛型結果(generics, <V>
).
下面演示如何將子線程返回的結果傳給主線程. Java提供了 FutureTask
類, 一般和 Callable
一起使用, 但請注意, FutureTask#get()
方法會阻塞調用的線程.
例如, 開新線程來計算金額(從1到100), 並將結果返回給主線程.
private static void doTaskWithResultInWorker() {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Task starts");
Thread.sleep(1000);
int result = 0;
for (int i=0; i<=100; i++) {
result += i;
}
System.out.println("Task finished and return result");
return result;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
try {
System.out.println("Before futureTask.get()");
System.out.println("Result:" + futureTask.get());
System.out.println("After futureTask.get()");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
結果如下:
Before futureTask.get()
Task starts
Task finished and return result
Result: 5050
After futureTask.get()
可以看到, 主線程調用 futureTask.get()
方法時被阻塞; 然後開始執行 Callable
內部的任務並返回結果; 接着 futureTask.get()
獲取結果, 主線程才繼續運行.
使用 FutureTask
和 Callable
, 可以直接在主線程得到子線程的執行結果, 但這會阻塞主線程. 如果不想阻塞主線程, 可以將 FutureTask
交給線程池來執行(使用ExecutorService
).
總結
多線程(Multithreading)是現代編程語言都具有的共同特徵. 其中, 線程間通信(inter-thread communication), 線程同步(thread synchronization), 線程安全(thread safety) 都是非常重要的知識.
原文鏈接: https://www.tutorialdocs.com/article/java-inter-thread-communication.html
原文日期: 2019年01月22日
翻譯日期: 2019年03月12日
翻譯人員: 鐵錨 - https://renfufei.blog.csdn.net/