Java正確地終止線程

首先要明確的是Java沒有提供任何機制來安全的終止線程。

Java雖然提供了stop方法來終止線程,但是這種方式簡單粗暴,很可能造成數據不一致的情況,因此stop方法已經棄用了。

目前較爲安全地終止線程方式有兩種:

1)使用標誌位,讓線程run方法在合適的時候結束執行,從而終止線程。

2)使用中斷機制,讓線程run方法在中斷的狀態下結束執行,從而終止線程。

ps:這兩種方案雖然都可以安全的終止線程,但是使用標誌位往往具有侷限性,譬如使用阻塞容器的線程,在容器阻塞的情況下,往往沒有機會檢測到到標誌位,從而導致線程無法終止(後面會有實例對比來說明這個問題)。所以,就有下面的結論:

通常,使用中斷機制是實現線程終止的最合理方式。

一、使用標誌位終止線程

如下實例聲明瞭一個volatile類型的boolean變量作爲標誌位,表示線程是否終止,當標誌位爲false時線程才保持運行。

    /**
     * 通過標誌位來停止線程
     */
    static class CancelThreadWorker extends Thread {
        private volatile boolean cancelled = false;

        public CancelThreadWorker(String name) {
            super(name);
        }

        public void run() {
            int i = 0;
            while (!cancelled) {
                try {
                    System.out.println(i);
                    Thread.sleep(2000);
                    i++;
                } catch (InterruptedException e) {
                    ExceptionUtil.printException(e);
                }
            }
        }

        public void cancel() {
            cancelled = true;
        }
    }

ps:爲了後期測試方便,寫個簡單的工具類MyThreadUtil對基礎打印操作做了封裝,如下:

    static class MyThreadUtil {
        public static void printException(Exception e) {
            System.out.println(Thread.currentThread().getName() + " throws an Exception:" + e);
        }
        public static void printState(Thread thread) {
            System.out.println(thread.getName() + " current state is:" + thread.getState());
        }
    }

main方法測試代碼如下:

        CancelThreadWorker cancelThreadWorker = new CancelThreadWorker("cancelThreadWorker");
        cancelThreadWorker.start();
        try {
            Thread.sleep(2500);
        } catch (InterruptedException e) {
            ExceptionUtil.printException(e);
        }
        cancelThreadWorker.cancel();
        while (cancelThreadWorker.isAlive()) {} //阻塞一小段時間,等待線程終止
        System.out.println(cancelThreadWorker.getState());

控制檯輸出信息如下:

0
1
cancelThreadWorker current state is:TERMINATED

可以看出使用標誌位確實讓線程終止了。

二、使用中斷機制

中斷是一種協作機制,調用interrupt方法並不代表線程立刻就停止了,而是告訴線程你應該停止了。

Java中設計一個良好的api通常要考慮對中斷做出及時響應,方便調用者知道被中斷的線程接收到了中斷信息,從而可以提前結束線程。例如Thread.sleep,Object.wait,Thread.join,阻塞容器的take和put,這類方法之所以被設計成拋出中斷異常InterruptedException,就是爲了對中斷做出及時響應,告訴調用者,我已經收到中斷信號了!值得注意的是,這類庫方法在拋出InterruptedException的時候會清除線程的中斷狀態。

如下的實例演示瞭如何通過中斷機制來終止線程,通過判斷當前線程中斷狀態是否爲false,僅當中斷狀態爲false時run方法才執行。

當外部線程調用了該線程的interrupt方法時,sleep方法會對中斷做出響應,拋出InterruptedException,繼而重置中斷狀態爲false。由於run方法限制了異常只能捕獲,所以我們必須對異常及時處理,這裏處理方案有兩種,1)重新調用當前線程的interrupt方法讓中斷狀態恢復爲true。2)直接使用break退出循環即可

    static class InterruptThreadWorker extends Thread {
        public InterruptThreadWorker(String name) {
            super(name);
        }

        public void run() {
            int i = 0;
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println(i);
                    Thread.sleep(2000);
                    i++;
                } catch (InterruptedException e) {
                    MyThreadUtil.printException(e);
                    Thread.currentThread().interrupt(); //由於中斷異常會清除中斷標記,所以此處一定要重新調用中斷方法。當然也可以用break來代替
//                    break; //使用break代替上面的interrupt也可以
                }
            }
        }
    }

main方法測試代碼如下:

        InterruptThreadWorker interruptThreadWorker = new InterruptThreadWorker("interruptThreadWorker");
        interruptThreadWorker.start();
        try {
            Thread.sleep(2500);
        } catch (InterruptedException e) {
            MyThreadUtil.printException(e);
        }
        interruptThreadWorker.interrupt(); //傳遞中斷信號,告訴interruptThreadWorker,你該終止了哈
        while (interruptThreadWorker.isAlive()) {} //阻塞一小段時間,等待線程終止
        MyThreadUtil.printState(interruptThreadWorker);

控制檯輸出如下:

0
1
interruptThreadWorker throws an Exception:java.lang.InterruptedException: sleep interrupted
interruptThreadWorker current state is:TERMINATED

可以看出在這個案例和上一個案例的演示結果是一樣的。

三、使用標誌位方案終止線程的侷限性

前面提到過標誌位的終止方案對於阻塞容器來說可能會失效,如下是一個具體的案例,源於<Java併發編程實戰>這本書。

    /**
     * 素數生產者線程
     */
    static class PrimeProducer extends Thread {

        private final BlockingQueue<BigInteger> queue;
        private volatile boolean cancelled = false;

        public PrimeProducer(BlockingQueue<BigInteger> queue) {
            this.queue = queue;
        }

        public void run() {
            BigInteger p = BigInteger.ONE.ONE;
            while (!cancelled) {
                try {
                    queue.put(p = p.nextProbablePrime());
                } catch (InterruptedException e) {
                    MyThreadUtil.printException(e);
                }
            }
        }

        public void cancel() {
            cancelled = true;
        }
    }

素數生產者線程PrimeProducer維護了一個阻塞容器BlockingQueue,run方法使用while循環判斷標誌位cancelled是否爲false,如果滿足則向容器中添加素數。

main方法的測試代碼如下,我們設置阻塞隊列的長度爲9,主線程睡眠20ms之後,設置標誌位爲false。

        BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<BigInteger>(9);
        PrimeProducer brokenPrimeProducer = new PrimeProducer(primes);
        brokenPrimeProducer.start();
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            MyThreadUtil.printException(e);
        }
        brokenPrimeProducer.cancel();
        System.out.println(brokenPrimeProducer.queue.size());
        while (brokenPrimeProducer.isAlive()) {} //阻塞一小段時間,等待線程終止
        MyThreadUtil.printState(brokenPrimeProducer);

控制檯打印如下:

9

細心觀察,發現控制檯其實一直處於運行狀態,並沒有打印這個線程的狀態,也就是說線程並沒有被終止,而是一直處於阻塞狀態。

這是因爲:線程開啓的20ms之後,阻塞隊列已經被填滿,而put作爲一個阻塞方法(類似的take),如果隊列滿則會持續到阻塞到隊列有可用空間爲止。本例中只有生成者線程而沒有消費者線程,導致put方法持續阻塞,所以無法檢測到while循環標誌位的狀態,故線程無法終止。

如果我們將main方法中的阻塞隊列擴大,線程會被正常終止的。只需修改下面一行:

BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<BigInteger>(9999);

或者:你也可以保持隊列容量不變,將主線程的睡眠時間縮小:

 Thread.sleep(2);

採用第一種實驗方案,控制檯打印如下:

56
Thread-0 current state is:TERMINATED

可以看出阻塞隊列在添加完第56個元素之後,發現標誌位爲true,不再添加元素,線程終止。證明前面的分析完全成立。

顯然實際項目開發中,我們不可能爲了終止線程來刻意改變阻塞隊列大小,這是一種投機行爲,包含了運氣成分,它可能會影響項目質量。所以對於包含阻塞隊列的線程來說,不提倡使用標誌位的策略終止線程。

本案例中對於素數生產者線程來說,使用中斷是最好的策略,將PrimeProducer改造成以下形式:

    /**
     * 素數生產者線程
     */
    static class PrimeProducer extends Thread {

        private final BlockingQueue<BigInteger> queue;

        public PrimeProducer(BlockingQueue<BigInteger> queue) {
            this.queue = queue;
        }

        public void run() {
            BigInteger p = BigInteger.ONE.ONE;
            while (!isInterrupted()) {
                try {
                    queue.put(p = p.nextProbablePrime());
                } catch (InterruptedException e) {
                    MyThreadUtil.printException(e);
                    cancel();
                }
            }
        }

        public void cancel() {
            this.interrupt();
        }
    }

依舊設置阻塞隊列大小爲9,延時20ms,main方法運行效果如下,可以看出控制檯打印之後正常結束,線程不再阻塞。

9
Thread-0 throws an Exception:java.lang.InterruptedException
Thread-0 current state is:TERMINATED

四、stop方法不安全的體現

stop方法不適合用來終止線程,是因爲它是強行結束線程,如果對象是一種不連貫的狀態,即使方法是同步的,也會立刻釋放已持有的鎖,從而造成數據不一致。

如下的案例演示這個問題:

static class UnstableObject {
        private String first = "ja";
        private String second = "va";

        public synchronized void setValues(String first, String second) throws Exception{
            this.first = first;
            Thread.sleep(10000);
            this.second = second;
        }

        public String getFirst() {
            return first;
        }

        public void setFirst(String first) {
            this.first = first;
        }

        public String getSecond() {
            return second;
        }

        public void setSecond(String second) {
            this.second = second;
        }
    }

UntableObject這個類維護了兩個屬性first和second,並提供了一個同步方法setValues對兩個屬性分別賦值,然而賦值過程並不是連貫的,存在一個睡眠10s的過程,如果一個線程調用setValues並在10s內進行被stop,則只會對第一個屬性成功賦值,第二個屬性仍然保持默認。

我們測試代碼如下:

        UnstableObject unstableObject = new UnstableObject();
        Thread thread = new Thread(()->{
            try {
                unstableObject.setValues("p", "hp");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            MyThreadUtil.printException(e);
        }
        thread.stop();
        MyThreadUtil.printState(thread);
        System.out.println(unstableObject.getFirst() +  unstableObject.getSecond());

控制檯輸出如下:

Thread-0 current state is:TERMINATED
pva

可以看出線程雖然終止了,但我們渴望輸出php,它卻輸出了pva,這就是stop不安全的一個體現。

 

 

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