本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
本節繼續上節的內容,探討如何使用wait/notify實現更多的協作場景。
同時開始
同時開始,類似於運動員比賽,在聽到比賽開始槍響後同時開始,下面,我們模擬下這個過程,這裡,有一個主執行緒和N個子執行緒,每個子執行緒模擬一個運動員,主執行緒模擬裁判,它們協作的共享變數是一個開始訊號。我們用一個類FireFlag來表示這個協作物件,程式碼如下所示:
static class FireFlag {
private volatile boolean fired = false;
public synchronized void waitForFire() throws InterruptedException {
while (!fired) {
wait();
}
}
public synchronized void fire() {
this.fired = true;
notifyAll();
}
}
複製程式碼
子執行緒應該呼叫waitForFire()等待槍響,而主執行緒應該呼叫fire()發射比賽開始訊號。
表示比賽運動員的類如下:
static class Racer extends Thread {
FireFlag fireFlag;
public Racer(FireFlag fireFlag) {
this.fireFlag = fireFlag;
}
@Override
public void run() {
try {
this.fireFlag.waitForFire();
System.out.println("start run "
+ Thread.currentThread().getName());
} catch (InterruptedException e) {
}
}
}
複製程式碼
主程式程式碼如下所示:
public static void main(String[] args) throws InterruptedException {
int num = 10;
FireFlag fireFlag = new FireFlag();
Thread[] racers = new Thread[num];
for (int i = 0; i < num; i++) {
racers[i] = new Racer(fireFlag);
racers[i].start();
}
Thread.sleep(1000);
fireFlag.fire();
}
複製程式碼
這裡,啟動了10個子執行緒,每個子執行緒啟動後等待fire訊號,主執行緒呼叫fire()後各個子執行緒才開始執行後續操作。
等待結束
理解join
在理解Synchronized一節中我們使用join方法讓主執行緒等待子執行緒結束,join實際上就是呼叫了wait,其主要程式碼是:
while (isAlive()) {
wait(0);
}
複製程式碼
只要執行緒是活著的,isAlive()返回true,join就一直等待。誰來通知它呢?當執行緒執行結束的時候,Java系統呼叫notifyAll來通知。
使用協作物件
使用join有時比較麻煩,需要主執行緒逐一等待每個子執行緒。這裡,我們演示一種新的寫法。主執行緒與各個子執行緒協作的共享變數是一個數,這個數表示未完成的執行緒個數,初始值為子執行緒個數,主執行緒等待該值變為0,而每個子執行緒結束後都將該值減一,當減為0時呼叫notifyAll,我們用MyLatch來表示這個協作物件,示例程式碼如下:
public class MyLatch {
private int count;
public MyLatch(int count) {
this.count = count;
}
public synchronized void await() throws InterruptedException {
while (count > 0) {
wait();
}
}
public synchronized void countDown() {
count--;
if (count <= 0) {
notifyAll();
}
}
}
複製程式碼
這裡,MyLatch構造方法的引數count應初始化為子執行緒的個數,主執行緒應該呼叫await(),而子執行緒在執行完後應該呼叫countDown()。
工作子執行緒的示例程式碼如下:
static class Worker extends Thread {
MyLatch latch;
public Worker(MyLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
// simulate working on task
Thread.sleep((int) (Math.random() * 1000));
this.latch.countDown();
} catch (InterruptedException e) {
}
}
}
複製程式碼
主執行緒的示例程式碼如下:
public static void main(String[] args) throws InterruptedException {
int workerNum = 100;
MyLatch latch = new MyLatch(workerNum);
Worker[] workers = new Worker[workerNum];
for (int i = 0; i < workerNum; i++) {
workers[i] = new Worker(latch);
workers[i].start();
}
latch.await();
System.out.println("collect worker results");
}
複製程式碼
MyLatch是一個用於同步協作的工具類,主要用於演示基本原理,在Java中有一個專門的同步類CountDownLatch,在實際開發中應該使用它,關於CountDownLatch,我們會在後續章節介紹。
MyLatch的功能是比較通用的,它也可以應用於上面"同時開始"的場景,初始值設為1,Racer類呼叫await(),主執行緒呼叫countDown()即可,如下所示:
public class RacerWithLatchDemo {
static class Racer extends Thread {
MyLatch latch;
public Racer(MyLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
this.latch.await();
System.out.println("start run "
+ Thread.currentThread().getName());
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
int num = 10;
MyLatch latch = new MyLatch(1);
Thread[] racers = new Thread[num];
for (int i = 0; i < num; i++) {
racers[i] = new Racer(latch);
racers[i].start();
}
Thread.sleep(1000);
latch.countDown();
}
}
複製程式碼
非同步結果
在主從模式中,手工建立執行緒往往比較麻煩,一種常見的模式是非同步呼叫,非同步呼叫返回一個一般稱為Promise或Future的物件,通過它可以獲得最終的結果。在Java中,表示子任務的介面是Callable,宣告為:
public interface Callable<V> {
V call() throws Exception;
}
複製程式碼
為表示非同步呼叫的結果,我們定義一個介面MyFuture,如下所示:
public interface MyFuture <V> {
V get() throws Exception ;
}
複製程式碼
這個介面的get方法返回真正的結果,如果結果還沒有計算完成,get會阻塞直到計算完成,如果呼叫過程發生異常,則get方法丟擲呼叫過程中的異常。
為方便主執行緒呼叫子任務,我們定義一個類MyExecutor,其中定義一個public方法execute,表示執行子任務並返回非同步結果,宣告如下:
public <V> MyFuture<V> execute(final Callable<V> task)
複製程式碼
利用該方法,對於主執行緒,它就不需要建立並管理子執行緒了,並且可以方便地獲取非同步呼叫的結果,比如,在主執行緒中,可以類似這樣啟動非同步呼叫並獲取結果:
public static void main(String[] args) {
MyExecutor executor = new MyExecutor();
// 子任務
Callable<Integer> subTask = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// ... 執行非同步任務
int millis = (int) (Math.random() * 1000);
Thread.sleep(millis);
return millis;
}
};
// 非同步呼叫,返回一個MyFuture物件
MyFuture<Integer> future = executor.execute(subTask);
// ... 執行其他操作
try {
// 獲取非同步呼叫的結果
Integer result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
MyExecutor的execute方法是怎麼實現的呢?它封裝了建立子執行緒,同步獲取結果的過程,它會建立一個執行子執行緒,該子執行緒的程式碼如下所示:
static class ExecuteThread<V> extends Thread {
private V result = null;
private Exception exception = null;
private boolean done = false;
private Callable<V> task;
private Object lock;
public ExecuteThread(Callable<V> task, Object lock) {
this.task = task;
this.lock = lock;
}
@Override
public void run() {
try {
result = task.call();
} catch (Exception e) {
exception = e;
} finally {
synchronized (lock) {
done = true;
lock.notifyAll();
}
}
}
public V getResult() {
return result;
}
public boolean isDone() {
return done;
}
public Exception getException() {
return exception;
}
}
複製程式碼
這個子執行緒執行實際的子任務,記錄執行結果到result變數、異常到exception變數,執行結束後設定共享狀態變數done為true並呼叫notifyAll以喚醒可能在等待結果的主執行緒。
MyExecutor的execute的方法的程式碼為:
public <V> MyFuture<V> execute(final Callable<V> task) {
final Object lock = new Object();
final ExecuteThread<V> thread = new ExecuteThread<>(task, lock);
thread.start();
MyFuture<V> future = new MyFuture<V>() {
@Override
public V get() throws Exception {
synchronized (lock) {
while (!thread.isDone()) {
try {
lock.wait();
} catch (InterruptedException e) {
}
}
if (thread.getException() != null) {
throw thread.getException();
}
return thread.getResult();
}
}
};
return future;
}
複製程式碼
execute啟動一個執行緒,並返回MyFuture物件,MyFuture的get方法會阻塞等待直到執行緒執行結束。
以上的MyExecutore和MyFuture主要用於演示基本原理,實際上,Java中已經包含了一套完善的框架Executors,相關的部分介面和類有:
- 表示非同步結果的介面Future和實現類FutureTask
- 用於執行非同步任務的介面Executor、以及有更多功能的子介面ExecutorService
- 用於建立Executor和ExecutorService的工廠方法類Executors
後續章節,我們會詳細介紹這套框架。
集合點
各個執行緒先是分頭行動,然後各自到達一個集合點,在集合點需要集齊所有執行緒,交換資料,然後再進行下一步動作。怎麼表示這種協作呢?協作的共享變數依然是一個數,這個數表示未到集合點的執行緒個數,初始值為子執行緒個數,每個執行緒到達集合點後將該值減一,如果不為0,表示還有別的執行緒未到,進行等待,如果變為0,表示自己是最後一個到的,呼叫notifyAll喚醒所有執行緒。我們用AssemblePoint類來表示這個協作物件,示例程式碼如下:
public class AssemblePoint {
private int n;
public AssemblePoint(int n) {
this.n = n;
}
public synchronized void await() throws InterruptedException {
if (n > 0) {
n--;
if (n == 0) {
notifyAll();
} else {
while (n != 0) {
wait();
}
}
}
}
}
複製程式碼
多個遊客執行緒,各自先獨立執行,然後使用該協作物件到達集合點進行同步的示例程式碼如下:
public class AssemblePointDemo {
static class Tourist extends Thread {
AssemblePoint ap;
public Tourist(AssemblePoint ap) {
this.ap = ap;
}
@Override
public void run() {
try {
// 模擬先各自獨立執行
Thread.sleep((int) (Math.random() * 1000));
// 集合
ap.await();
System.out.println("arrived");
// ... 集合後執行其他操作
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
int num = 10;
Tourist[] threads = new Tourist[num];
AssemblePoint ap = new AssemblePoint(num);
for (int i = 0; i < num; i++) {
threads[i] = new Tourist(ap);
threads[i].start();
}
}
}
複製程式碼
這裡實現的是AssemblePoint主要用於演示基本原理,Java中有一個專門的同步工具類CyclicBarrier可以替代它,關於該類,我們後續章節介紹。
小結
上節和本節介紹了Java中執行緒間協作的基本機制wait/notify,協作關鍵要想清楚協作的共享變數和條件是什麼,為進一步理解,針對多種協作場景,我們演示了wait/notify的用法及基本協作原理,Java中有專門為協作而建的阻塞佇列、同步工具類、以及Executors框架,我們會在後續章節介紹,在實際開發中,應該儘量使用這些現成的類,而非重新發明輪子。
之前,我們多次碰到了InterruptedException並選擇了忽略,現在是時候進一步瞭解它了。
(與其他章節一樣,本節所有程式碼位於 github.com/swiftma/pro…)
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。