Java進階知識 - 線程間通信

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

這就實現了需要的效果.

主流程說明

  1. 首先創建一個對象鎖: lock = new Object();
  2. A獲取鎖, 得到後先打印1, 然後調用 lock.wait() 進入等待狀態, 同時移交鎖的控制權;
  3. B暫時不會執行打印, 需要等A調用lock.wait()釋放鎖之後, B 獲得鎖纔開始執行打印;
  4. B打印出1、2、3, 然後調用lock.notify()方法來喚醒等待這個鎖的線程(A);
  5. 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 來實現這種類型的通信. 其基本用法爲:

  1. 創建一個計數器(counter), 並設置初始值: CountdownLatch countDownLatch = new CountDownLatch(3);
  2. 需要等待的線程, 調用 countDownLatch.await() 方法進入等待狀態, 直到 count 值變成0爲止;
  3. 其他線程調用 countDownLatch.countDown() 來將 count 值減小;
  4. 當其他線程調用 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, 其基本用法爲:

  1. 首先創建一個公開的 CyclicBarrier 對象, 並設置同時等待的線程數量, CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
  2. 這些線程各自進行準備, 自身準備好之後, 還需要等其他線程準備完畢, 即調用 cyclicBarrier.await() 方法來等待;
  3. 當需要同時等待的線程全部調用了 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() 獲取結果, 主線程才繼續運行.

使用 FutureTaskCallable, 可以直接在主線程得到子線程的執行結果, 但這會阻塞主線程. 如果不想阻塞主線程, 可以將 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/

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