1. 程式人生 > >正確實現子執行緒任務全部完成後主執行緒關閉的四種方法

正確實現子執行緒任務全部完成後主執行緒關閉的四種方法

寫程式碼過程中遇到了這樣的場景, 需要觀察各個子執行緒執行的情況, 如果不進行任何處理當main 方法執行完畢後其他子執行緒都會關閉, 無法觀察到所有子執行緒的詳細的執行情況, 於是需要讓主執行緒等待所有子執行緒執行完畢後才關閉, 以前比較馬虎的做法就是在main

函式裡面新增Thread.sleep(time) 但是這種方法始終不完美, 因為需要人為的設定等待時間, 不是最佳的實踐, 於是查閱了一些資料以及部落格, 此處總結一下實現這個目的的四種方法.

方法一 Thread.sleep

這應該是最常見的方法, 雖然不是最佳的實踐, 但是可以勉強滿足需求.

public static void main(String[] args) throws InterruptedException{
        for(int i=0; i<10; i++){
            new Thread("subthread" + i){
                @Override
                public
void run() { try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + "finished"); }catch(InterruptedException e){ e.printStackTrace(); } } }.start(); } Thread.sleep(2000
); System.out.println("main thread finished"); }

執行結果:

subthread0finished
subthread1finished
subthread2finished
subthread6finished
subthread4finished
subthread5finished
subthread3finished
subthread9finished
subthread8finished
subthread7finished
main thread finished

方法二 ExecutorService

可以使用執行緒池實現, 常用的執行緒池物件都是ExecutorService 介面的實現, 其中提供了shutdown 等方法可以保證當前提交的任務在子執行緒上執行完畢後 Java程序正常退出.

public static void main(String[] args) {
        // 建立一個ExecutorService
        ExecutorService ex =  Executors.newCachedThreadPool();
        for(int i=0; i<10; i++){
            // 新增 task
            ex.submit(new Thread("subthread" + i){

                @Override
                public void run() {
                    try{
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + "finished");
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
            });
        }

        // 使用shutdown 會通知executorservice 停止新增其它task 它會等待所有子執行緒執行結束才退出Java程序
        ex.shutdown();
        System.out.println("main thread finished");

    }

執行結果為:

main thread finished
pool-1-thread-3finished
pool-1-thread-4finished
pool-1-thread-2finished
pool-1-thread-1finished
pool-1-thread-5finished
pool-1-thread-7finished
pool-1-thread-8finished
pool-1-thread-6finished
pool-1-thread-9finished
pool-1-thread-10finished

此種方法有一些小的瑕疵, 我們從輸出的資訊可以看到主執行緒其實先於子執行緒執行完畢, 所以這種方法只能夠保證子執行緒在程式退出之前可以執行完, 但是不能夠保證主執行緒在子執行緒執行完畢之後再執行.所以程式碼還需要更改, 新增一個awaitTermination(time, timeunit) 設定一個較為合理的等待的時間, 等待子執行緒執行完畢.

public static void main(String[] args) {
        // 建立一個ExecutorService
        ExecutorService ex =  Executors.newCachedThreadPool();
        for(int i=0; i<10; i++){
            // 新增 task
            ex.submit(new Thread("subthread" + i){

                @Override
                public void run() {
                    try{
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + "finished");
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
            });
        }

        // 使用shutdown 會通知executorservice 停止新增其它task 它會等待所有子執行緒執行結束才退出Java程序
        ex.shutdown();
        try {
            // 設定等待時間等待子執行緒執行完畢
            if(!ex.awaitTermination(2000, TimeUnit.MILLISECONDS)){
                // 等待時間內子執行緒並未全部執行完畢就直接關閉
                ex.shutdownNow();
            }
        }catch(InterruptedException e){
            ex.shutdownNow();
        }

        System.out.println("main thread finished");

    }

執行結果:

pool-1-thread-1finished
pool-1-thread-5finished
pool-1-thread-4finished
pool-1-thread-9finished
pool-1-thread-8finished
pool-1-thread-3finished
pool-1-thread-2finished
pool-1-thread-7finished
pool-1-thread-6finished
pool-1-thread-10finished
main thread finished

可以看到主執行緒執行的內容在最後輸出的. 這個方法與方法一一樣都需要設定等待時間, 不是很完美的方法.

方法三 thread.join

thread.join 表示執行這段程式碼的執行緒會處於掛起狀態, 等待呼叫這個方法的執行緒(此處就是這個thread) 執行完畢後才繼續執行. 下面的例子中子執行緒都是在main 執行緒上面建立的, 所以在main 執行緒裡面執行某一個子執行緒.join 時會等待子執行緒執行完畢才繼續執行main 執行緒, 程式碼如下:

public static void main(String[] args) throws InterruptedException{
        List<Thread> list = new ArrayList<>();
        for(int i=0; i<10; i++){
            Thread t = new Thread("subthread" + i){
                @Override
                public void run() {
                    try{
                        Thread.sleep(3000);
                        System.out.println(Thread.currentThread().getName() + "finished");
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
            };
            list.add(t);
            t.start();
        }
        for(Thread item : list){
            item.join();
        }
        System.out.println("main thread finished");
    }

執行結果:

subthread1finished
subthread2finished
subthread0finished
subthread3finished
subthread6finished
subthread5finished
subthread4finished
subthread9finished
subthread7finished
subthread8finished
main thread finished

使用這個方法相比去上面兩種方法優點在於不用設定等待時間.

方法四 Thread.yield and Thread.activeCount

首先說明一下這兩個方法的作用, Thread.yield 通俗講就是讓步, 呼叫這個方法的當前執行緒放棄 自己佔用CPU 的權利. 但是並不表示當前執行緒一定不再執行. Thread.activeCount 方法返回的是當前呼叫這個執行緒對應的執行緒組中所有的活躍執行緒數量. 在建立執行緒的時候(new Thread) 其中這個ThreadGroup 引數指的就是建立執行緒對應的執行緒組, 如果這個引數沒有指定, 那麼建立的執行緒與建立這個執行緒的執行緒是同一個執行緒組. 程式碼如下:

public static void main(String[] args) {
        // 關鍵引數
        int defaultThreadNum = 2;
        for(int i=0; i<10 ;i++){
            new Thread("subthread" + i){
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + "finished");
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }.start();
        }

        while(Thread.activeCount() > defaultThreadNum){
            // 當活躍執行緒數大於設定的預設執行緒數的時候 主執行緒讓步
            Thread.yield();
        }
        System.out.println("main thread finished");
    }

執行結果:

subthread0finished
subthread4finished
subthread1finished
subthread8finished
subthread9finished
subthread5finished
subthread2finished
subthread3finished
subthread6finished
subthread7finished
main thread finished

有一個很關鍵的地方就是這個defaultthreadnum 設定的是2. 有一些blog中設定的是1, 但是1會導致無限迴圈, 主執行緒無法退出, 原因在於大家都認為主執行緒所在的執行緒組中排除子執行緒後只剩主執行緒一個執行緒了, 其實不然, 比如我們執行如下的程式碼:

public static void main(String[] args) {
        Thread.currentThread().getThreadGroup().list();
    }

輸出:

java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[main,5,main]
    Thread[Monitor Ctrl-Break,5,main]

可以看到主執行緒所在的執行緒組中還有一個叫Monitor Ctrl-Break 的執行緒, 因此排除所有子執行緒後還剩餘2個執行緒, 所以迴圈判斷的門限(defaultThreadNum) 需要設定為2.
此方法也是不需要設定等待時間的.

綜上所述, 如果需要實現主執行緒在所有子執行緒執行結束後再執行可以使用方法三和方法四.