1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Java 是如何實(shí)現(xiàn)線程間通信的?

        共 5679字,需瀏覽 12分鐘

         ·

        2021-10-25 00:41

        點(diǎn)擊“藍(lán)字”,關(guān)注,置頂公眾號(hào)

        每日技術(shù)干貨,第一時(shí)間送達(dá)!


        正常情況下,每個(gè)子線程完成各自的任務(wù)就可以結(jié)束了。不過有的時(shí)候,我們希望多個(gè)線程協(xié)同工作來(lái)完成某個(gè)任務(wù),這時(shí)就涉及到了線程間通信了。


        本文涉及到的知識(shí)點(diǎn):


        1. thread.join(),

        2. object.wait(),

        3. object.notify(),

        4. CountdownLatch,

        5. CyclicBarrier,

        6. FutureTask,

        7. Callable 。

        本文涉及代碼:
        https://github.com/wingjay/HelloJava/blob/master/multi-thread/src/ForArticle.java

        下面我從幾個(gè)例子作為切入點(diǎn)來(lái)講解下 Java 里有哪些方法來(lái)實(shí)現(xiàn)線程間通信。


        1. 如何讓兩個(gè)線程依次執(zhí)行?

        2. 那如何讓 兩個(gè)線程按照指定方式有序交叉運(yùn)行呢?

        3. 四個(gè)線程 A B C D,其中 D 要等到 A B C 全執(zhí)行完畢后才執(zhí)行,而且 A B C 是同步運(yùn)行的

        4. 三個(gè)運(yùn)動(dòng)員各自準(zhǔn)備,等到三個(gè)人都準(zhǔn)備好后,再一起跑

        5. 子線程完成某件任務(wù)后,把得到的結(jié)果回傳給主線程


        如何讓兩個(gè)線程依次執(zhí)行?


        假設(shè)有兩個(gè)線程,一個(gè)是線程 A,另一個(gè)是線程 B,兩個(gè)線程分別依次打印 1-3 三個(gè)數(shù)字即可。我們來(lái)看下代碼:

        private static void demo1() {    Thread A = new Thread(new Runnable() {@Overridepublic void run() {            printNumber("A");        }    });    Thread B = new Thread(new Runnable() {@Overridepublic void run() {            printNumber("B");        }    });    A.start();    B.start();}

        其中的 printNumber(String) 實(shí)現(xiàn)如下,用來(lái)依次打印 1, 2, 3 三個(gè)數(shù)字:

        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);    }}

        這時(shí)我們得到的結(jié)果是:

        B print: 1
        A print: 1
        B print: 2
        A print: 2
        B print: 3
        A print: 3

        可以看到 A 和 B 是同時(shí)打印的。


        那么,如果我們希望 B 在 A 全部打印 完后再開始打印呢?我們可以利用 thread.join() 方法,代碼如下:

        private static void demo2() {    Thread A = new Thread(new Runnable() {        @Overridepublic void run() {            printNumber("A");        }    });    Thread B = new Thread(new Runnable() {        @Overridepublic void run() {            System.out.println("B 開始等待 A");try {                A.join();            } catch (InterruptedException e) {                e.printStackTrace();            }            printNumber("B");        }    });    B.start();    A.start();}


        得到的結(jié)果如下:

        B 開始等待 A
        A print: 1
        A print: 2
        A print: 3

        B print: 1
        B print: 2
        B print: 3

        所以我們能看到 A.join() 方法會(huì)讓 B 一直等待直到 A 運(yùn)行完畢。


        那如何讓 兩個(gè)線程按照指定方式有序交叉運(yùn)行呢?


        還是上面那個(gè)例子,我現(xiàn)在希望 A 在打印完 1 后,再讓 B 打印 1, 2, 3,最后再回到 A 繼續(xù)打印 2, 3。這種需求下,顯然 Thread.join() 已經(jīng)不能滿足了。


        我們需要更細(xì)粒度的鎖來(lái)控制執(zhí)行順序。


        這里,我們可以利用 object.wait() 和 object.notify() 兩個(gè)方法來(lái)實(shí)現(xiàn)。代碼如下:

        /** * A 1, B 1, B 2, B 3, A 2, A 3 */private static void demo3() {    Object lock = new Object();    Thread A = new Thread(new Runnable() {        @Overridepublic void run() {            synchronized (lock) {                System.out.println("A 1");try {lock.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println("A 2");                System.out.println("A 3");            }        }    });    Thread B = new Thread(new Runnable() {        @Overridepublic void run() {            synchronized (lock) {                System.out.println("B 1");                System.out.println("B 2");                System.out.println("B 3");lock.notify();            }        }    });    A.start();    B.start();}


        打印結(jié)果如下:

        A 1
        A waiting…

        B 1
        B 2
        B 3
        A 2
        A 3

        正是我們要的結(jié)果。


        那么,這個(gè)過程發(fā)生了什么呢?


        1. 首先創(chuàng)建一個(gè) A 和 B 共享的對(duì)象鎖 lock = new Object();

        2. 當(dāng) A 得到鎖后,先打印 1,然后調(diào)用 lock.wait() 方法,交出鎖的控制權(quán),進(jìn)入 wait 狀態(tài);

        3. 對(duì) B 而言,由于 A 最開始得到了鎖,導(dǎo)致 B 無(wú)法執(zhí)行;直到 A 調(diào)用 lock.wait() 釋放控制權(quán)后, B 才得到了鎖;

        4. B 在得到鎖后打印 1, 2, 3;然后調(diào)用 lock.notify() 方法,喚醒正在 wait 的 A;

        5. A 被喚醒后,繼續(xù)打印剩下的 2,3。


        為了更好理解,我在上面的代碼里加上 log 方便讀者查看。

        private static void demo3() {    Object lock = new Object();    Thread A = new Thread(new Runnable() {        @Overridepublic void run() {            System.out.println("INFO: A 等待鎖 ");            synchronized (lock) {                System.out.println("INFO: A 得到了鎖 lock");                System.out.println("A 1");try {                    System.out.println("INFO: A 準(zhǔn)備進(jìn)入等待狀態(tài),放棄鎖 lock 的控制權(quán) ");lock.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println("INFO: 有人喚醒了 A, A 重新獲得鎖 lock");                System.out.println("A 2");                System.out.println("A 3");            }        }    });    Thread B = new Thread(new Runnable() {        @Overridepublic void run() {            System.out.println("INFO: B 等待鎖 ");            synchronized (lock) {                System.out.println("INFO: B 得到了鎖 lock");                System.out.println("B 1");                System.out.println("B 2");                System.out.println("B 3");                System.out.println("INFO: B 打印完畢,調(diào)用 notify 方法 ");lock.notify();            }        }    });    A.start();    B.start();}


        打印結(jié)果如下:

        INFO: A 等待鎖
        INFO: A 得到了鎖 lock
        A 1
        INFO: A 準(zhǔn)備進(jìn)入等待狀態(tài),調(diào)用 lock.wait() 放棄鎖 lock 的控制權(quán)
        INFO: B 等待鎖
        INFO: B 得到了鎖 lock
        B 1
        B 2
        B 3
        INFO: B 打印完畢,調(diào)用 lock.notify() 方法
        INFO: 有人喚醒了 A, A 重新獲得鎖 lock
        A 2
        A 3


        四個(gè)線程 A B C D,其中 D 要等到 A B C 全執(zhí)行完畢后才執(zhí)行,而且 A B C 是同步運(yùn)行的


        最開始我們介紹了 thread.join(),可以讓一個(gè)線程等另一個(gè)線程運(yùn)行完畢后再繼續(xù)執(zhí)行,那我們可以在 D 線程里依次 join A B C,不過這也就使得 A B C 必須依次執(zhí)行,而我們要的是這三者能同步運(yùn)行。


        或者說(shuō),我們希望達(dá)到的目的是:A B C 三個(gè)線程同時(shí)運(yùn)行,各自獨(dú)立運(yùn)行完后通知 D;對(duì) D 而言,只要 A B C 都運(yùn)行完了,D 再開始運(yùn)行。針對(duì)這種情況,我們可以利用 CountdownLatch 來(lái)實(shí)現(xiàn)這類通信方式。它的基本用法是:


        1. 創(chuàng)建一個(gè)計(jì)數(shù)器,設(shè)置初始值,CountdownLatch countDownLatch = new CountDownLatch(2);

        2. 在 等待線程 里調(diào)用 countDownLatch.await() 方法,進(jìn)入等待狀態(tài),直到計(jì)數(shù)值變成 0;

        3. 在 其他線程 里,調(diào)用 countDownLatch.countDown() 方法,該方法會(huì)將計(jì)數(shù)值減小 1;

        4. 當(dāng) 其他線程 的 countDown() 方法把計(jì)數(shù)值變成 0 時(shí),等待線程 里的 countDownLatch.await() 立即退出,繼續(xù)執(zhí)行下面的代碼。


        實(shí)現(xiàn)代碼如下:

        private static void runDAfterABC() {int worker = 3;    CountDownLatch countDownLatch = new CountDownLatch(worker);new Thread(new Runnable() {        @Overridepublic void run() {            System.out.println("D is waiting for other three threads");try {                countDownLatch.await();                System.out.println("All done, D starts working");            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }).start();for (char threadName='A'; threadName <= 'C'; threadName++) {        final String tN = String.valueOf(threadName);new Thread(new Runnable() {            @Overridepublic void run() {                System.out.println(tN + " is working");try {                    Thread.sleep(100);                } catch (Exception e) {                    e.printStackTrace();                }                System.out.println(tN + " finished");                countDownLatch.countDown();            }        }).start();    }}


        下面是運(yùn)行結(jié)果:

        D is waiting for other three threads
        A is working
        B is working
        C is working

        A finished
        C finished
        B finished
        All done, D starts working

        其實(shí)簡(jiǎn)單點(diǎn)來(lái)說(shuō),CountDownLatch 就是一個(gè)倒計(jì)數(shù)器,我們把初始計(jì)數(shù)值設(shè)置為3,當(dāng) D 運(yùn)行時(shí),先調(diào)用 countDownLatch.await() 檢查計(jì)數(shù)器值是否為 0,若不為 0 則保持等待狀態(tài);當(dāng)A B C 各自運(yùn)行完后都會(huì)利用countDownLatch.countDown(),將倒計(jì)數(shù)器減 1,當(dāng)三個(gè)都運(yùn)行完后,計(jì)數(shù)器被減至 0;此時(shí)立即觸發(fā) D 的 await() 運(yùn)行結(jié)束,繼續(xù)向下執(zhí)行。


        因此,CountDownLatch 適用于一個(gè)線程去等待多個(gè)線程的情況。


        三個(gè)運(yùn)動(dòng)員各自準(zhǔn)備,等到三個(gè)人都準(zhǔn)備好后,再一起跑


        上面是一個(gè)形象的比喻,針對(duì) 線程 A B C 各自開始準(zhǔn)備,直到三者都準(zhǔn)備完畢,然后再同時(shí)運(yùn)行 。也就是要實(shí)現(xiàn)一種 線程之間互相等待 的效果,那應(yīng)該怎么來(lái)實(shí)現(xiàn)呢?


        上面的 CountDownLatch 可以用來(lái)倒計(jì)數(shù),但當(dāng)計(jì)數(shù)完畢,只有一個(gè)線程的 await() 會(huì)得到響應(yīng),無(wú)法讓多個(gè)線程同時(shí)觸發(fā)。


        為了實(shí)現(xiàn)線程間互相等待這種需求,我們可以利用 CyclicBarrier 數(shù)據(jù)結(jié)構(gòu),它的基本用法是:


        1. 先創(chuàng)建一個(gè)公共 CyclicBarrier 對(duì)象,設(shè)置 同時(shí)等待 的線程數(shù),CyclicBarrier cyclicBarrier = new CyclicBarrier(3);

        2. 這些線程同時(shí)開始自己做準(zhǔn)備,自身準(zhǔn)備完畢后,需要等待別人準(zhǔn)備完畢,這時(shí)調(diào)用 cyclicBarrier.await(); 即可開始等待別人;

        3. 當(dāng)指定的 同時(shí)等待 的線程數(shù)都調(diào)用了 cyclicBarrier.await();時(shí),意味著這些線程都準(zhǔn)備完畢好,然后這些線程才 同時(shí)繼續(xù)執(zhí)行。


        實(shí)現(xiàn)代碼如下,設(shè)想有三個(gè)跑步運(yùn)動(dòng)員,各自準(zhǔn)備好后等待其他人,全部準(zhǔn)備好后才開始跑:

        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() {            @Overridepublic void run() {long prepareTime = random.nextInt(10000) + 100;                System.out.println(rN + " is preparing for time: " + prepareTime);try {                    Thread.sleep(prepareTime);                } catch (Exception e) {                    e.printStackTrace();                }try {                    System.out.println(rN + " is prepared, waiting for others");                    cyclicBarrier.await(); // 當(dāng)前運(yùn)動(dòng)員準(zhǔn)備完畢,等待別人準(zhǔn)備好                } catch (InterruptedException e) {                    e.printStackTrace();                } catch (BrokenBarrierException e) {                    e.printStackTrace();                }                System.out.println(rN + " starts running"); // 所有運(yùn)動(dòng)員都準(zhǔn)備好了,一起開始跑            }        }).start();    }}

        打印的結(jié)果如下:

        A is preparing for time: 4131
        B is preparing for time: 6349
        C is preparing for time: 8206
        A is prepared, waiting for others
        B is prepared, waiting for others
        C is prepared, waiting for others
        C starts running
        A starts running
        B starts running

        子線程完成某件任務(wù)后,把得到的結(jié)果回傳給主線程


        實(shí)際的開發(fā)中,我們經(jīng)常要?jiǎng)?chuàng)建子線程來(lái)做一些耗時(shí)任務(wù),然后把任務(wù)執(zhí)行結(jié)果回傳給主線程使用,這種情況在 Java 里要如何實(shí)現(xiàn)呢?


        回顧線程的創(chuàng)建,我們一般會(huì)把 Runnable 對(duì)象傳給 Thread 去執(zhí)行。Runnable定義如下:

        public interface Runnable {public abstract void run();}

        可以看到 run() 在執(zhí)行完后不會(huì)返回任何結(jié)果。那如果希望返回結(jié)果呢?這里可以利用另一個(gè)類似的接口類 Callable:

        @FunctionalInterfacepublic interface Callable {/**     * Computes a result, or throws an exception if unable to do so.     *     * @return computed result     * @throws Exception if unable to compute a result     */V call() throws Exception;}

        可以看出 Callable 最大區(qū)別就是返回范型 V 結(jié)果。


        那么下一個(gè)問題就是,如何把子線程的結(jié)果回傳回來(lái)呢?在 Java 里,有一個(gè)類是配合 Callable 使用的:FutureTask,不過注意,它獲取結(jié)果的 get 方法會(huì)阻塞主線程。


        舉例,我們想讓子線程去計(jì)算從 1 加到 100,并把算出的結(jié)果返回到主線程。

        private static void doTaskWithResultInWorker() {    Callable callable = new Callable() {        @Overridepublic 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 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();    }}

        打印結(jié)果如下:

        Before futureTask.get()
        Task starts
        Task finished and return result
        Result: 5050
        After futureTask.get()

        可以看到,主線程調(diào)用 futureTask.get() 方法時(shí)阻塞主線程;然后 Callable 內(nèi)部開始執(zhí)行,并返回運(yùn)算結(jié)果;此時(shí) futureTask.get() 得到結(jié)果,主線程恢復(fù)運(yùn)行。


        這里我們可以學(xué)到,通過 FutureTask 和 Callable 可以直接在主線程獲得子線程的運(yùn)算結(jié)果,只不過需要阻塞主線程。當(dāng)然,如果不希望阻塞主線程,可以考慮利用 ExecutorService,把 FutureTask 放到線程池去管理執(zhí)行。



        往期推薦



        YAML 文件的這些騷操作,你都會(huì)了嗎?

        理解SpringAop,看這篇就夠了!記得收藏

        DataGrip果然不一般

        RabbitMQ的幾種工作模式和優(yōu)化建議

        扔掉 Postman ,試試 IntelliJ IDEA 自帶的插件!

        Java8日期處理方式,日常工作必備!



        瀏覽 55
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            日本一区二区在线视频 | 苍井空无码播放 | 精品午夜一区二区三区在 | 日韩人妻一区二区 | 欧美色图888 | 丝袜美腿美女被狂躁在线观看 | 成人视频免费无码 | 91极品视觉盛宴 | 天天日天天色天天干 | 午夜色播 |