如何正確停止線程之錯誤的停止方法

該篇承接上一篇如何正確停止線程?(一)

錯誤的停止方法

被棄用的stop,suspend和resume方法

示例一:錯誤的停止方法:用stop()來停止線程,會導致線程運行一半突然停止,沒辦法完成一個基本單位的操作(一個連隊),會造成髒數據(有的連隊多領取少領取裝備)。

public class StopThread implements Runnable {

    @Override
    public void run() {
        //模擬指揮軍隊:一共有5個連隊,每個連隊10人,以連隊爲單位,發放武器彈藥,叫到號的士兵前去領取
        for (int i = 0; i < 5; i++) {
            System.out.println("連隊" + i + "開始領取武器");
            for (int j = 0; j < 10; j++) {
                System.out.println(j);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("連隊"+i+"已經領取完畢");
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new StopThread());
        thread.start();
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.stop();
    }
}

打印結果:

連隊0開始領取武器
0
1
2
3
4
5
6
7
8
9
連隊0已經領取完畢
連隊1開始領取武器
0
1
2
3
4
5

stop太強制,會導致原來的邏輯沒完整做完就停止了。

用volatile設置boolean標記位

  • 看上去可行

示例二:演示用volatile的侷限:part1 看似可行

public class WrongWayVolatile implements Runnable {

    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍數。");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatile r = new WrongWayVolatile();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(5000);
        r.canceled = true;
    }
}

打印結果:

0是100的倍數。
100是100的倍數
...
2700是100的倍數。
3200是100的倍數。

這種情況下通過volitile來中斷線程是可行的。

  • 錯誤原因

示例三:演示用volatile的侷限part2 陷入阻塞時,volatile是無法線程的 此例中,生產者的生產速度很快,消費者消費速度慢,所以阻塞隊列滿了以後,生產者會阻塞,等待消費者進一步消費

生產者:

class Producer implements Runnable {

    public volatile boolean canceled = false;

    BlockingQueue storage;

    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }


    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    storage.put(num);
                    System.out.println(num + "是100的倍數,被放到倉庫中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生產者結束運行");
        }
    }
}

消費者:

public class Consumer {

    BlockingQueue storage;

    public Consumer(BlockingQueue storage) {
        this.storage = storage;
    }

    public boolean needMoreNums() {
        if (Math.random() > 0.95) {
            return false;
        }
        return true;
    }
}

主類:

public class WrongWayVolatileCantStop {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take()+"被消費了");
            Thread.sleep(100);
        }
        System.out.println("消費者不需要更多數據了。");

        //一旦消費不需要更多數據了,我們應該讓生產者也停下來,但是實際情況
        producer.canceled=true;
        System.out.println(producer.canceled);
    }
}

打印結果:
示例三打印結果
我們發現volatile設置爲了true,但是生產者並沒有停下來。

爲什麼用volatile停止線程不夠全面?

  • 這種做法是錯誤的,或者說是不夠全面的,在某些情況下雖然可用,但是某些情況下有嚴重問題

  • 這種方法在《Java併發編程實戰》中被明確指出了缺陷,我們一起來看看缺陷在哪裏

  • 此方法錯誤的原因在於,如果我們遇到了線程長時間阻塞(這是一種很常見的情況,例如生產者消費者模式中就存在這樣的情況),就沒辦法及時喚醒它,或者永遠都無法喚醒該線程,而 interrupt設計之初就是把wait等長期阻塞作爲一種特殊情況考慮在內了,我們應該用 interrupt來停止線程。

  • 修正方式

示例四:用中斷來修復剛纔的無盡等待問題

public class WrongWayVolatileFixed {

    public static void main(String[] args) throws InterruptedException {

        WrongWayVolatileFixed body = new WrongWayVolatileFixed();
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        Producer producer = body.new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = body.new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take() + "被消費了");
            Thread.sleep(100);
        }
        System.out.println("消費者不需要更多數據了。");

        producerThread.interrupt();
    }


    class Producer implements Runnable {

        BlockingQueue storage;

        public Producer(BlockingQueue storage) {
            this.storage = storage;
        }


        @Override
        public void run() {
            int num = 0;
            try {
                while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        storage.put(num);
                        System.out.println(num + "是100的倍數,被放到倉庫中了。");
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生產者結束運行");
            }
        }
    }

    class Consumer {

        BlockingQueue storage;

        public Consumer(BlockingQueue storage) {
            this.storage = storage;
        }

        public boolean needMoreNums() {
            if (Math.random() > 0.95) {
                return false;
            }
            return true;
        }
    }
}

打印結果:

4200是100的倍數,被放到倉庫中了。
3300被消費了
4300是100的倍數,被放到倉庫中了。
3400被消費了
4400是100的倍數,被放到倉庫中了。
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2048)
	at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)
	at com.hd.thread.stop.wrong.WrongWayVolatileFixed$Producer.run(WrongWayVolatileFixed.java:50)
	at java.lang.Thread.run(Thread.java:745)
消費者不需要更多數據了。
生產者結束運行

使用interrupt,程序正常中斷了。

停止線程相關重要函數解析

判斷是否已被中斷相關方法

  • static boolean interrupted() 判斷當前線程是否被中斷,調用後會把中斷線程直接設爲false,即清除中斷標誌
  • boolean isInterrupted() 判斷當前線程是否被中斷,不會清除中斷標誌
  • Thread.interrupted()的目的對象

示例五:注意Thread.interrupted()方法的目標對象是“當前線程”,而不管本方法來自於哪個對象

public class RightWayInterrupted {

    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(() -> {
            for (; ; ) {
            }
        });

        // 啓動線程
        threadOne.start();
        //設置中斷標誌
        threadOne.interrupt();
        //獲取中斷標誌
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        //獲取中斷標誌並重置
        System.out.println("isInterrupted: " + threadOne.interrupted());
        //獲取中斷標誌並重直
        System.out.println("isInterrupted: " + Thread.interrupted());
        //獲取中斷標誌
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        threadOne.join();
        System.out.println("Main thread is over.");
    }
}

打印結果:

isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true

interrupted()是靜態方法無論是對象調用還是類調用判斷的都是主線程的中斷標誌,所以應該都是false。

面試問題

1.如何停止線程?

  1. 原理:用interrupt來請求、好處
  2. 想停止線程,要請求方、被停止方、子方法被調用方相互配合
  3. 最後再說錯誤的方法:stop/suspend已經廢棄,volatile的boolean無法處理長時間阻塞的情況。

面試題實戰:

如何停止線程?

A.用volatile的boolean作爲標記來停止

B.用stop()方法讓線程停止

C.用Interrupt來請求線程停止

解答

應該選C。

1.原理:用interrupt來請求線程停止而不是強制,好處是安全

2.想停止線程,要請求方、被停止方、子方法被調用方相互配合才行:

  • 作爲被停止方:每次循環中或者適時檢查中斷信號,並且在可能拋出nterrupedEXception的地方處理該中斷信號;

  • 請求方:發出中斷信

  • 子方法調用方(被線程調用的方法的作者)要注意:優先在方法層面拋出Interruped EXception,或者檢查到中斷信號時,再次設置中斷狀態;

3.最後再說錯誤的方法:stop/suspend已廢棄,volatile的boolean無法處理長時間阻塞的情況

2.如何處理不可中斷的阻塞

針對特定的情況使用特定的方法,儘可能做到響應中斷。

面試題實戰:

無法響應中斷時如何停止線程?

A.用 interrupt方法來請求停止線程

B.不可中斷的阻塞無法處理

C.根據不同的類調用不同的方法

解答

應該選C。

如果線程阻塞是由於調用了wait(),sleep()或join()方法,你可以中斷線程,通過拋出 InterruptedException異常來喚醒該線程。

但是對於不能響應 InterruptedException的阻塞,很遺憾,並沒有—個通用的解決方但是我們可以利用特定的其它的可以響應中斷的方法,比如ReentrantLock.lockInterruptibly(),比如關閉套接字使線程立即返回等方法來達到目的。

答案有很多種,因爲有很多原因會造成線程阻塞,所以針對不同情況,喚起的方法也不同。

總結就是說如果不支持響應中斷,就要用特定方法來喚起,沒有萬能藥。

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