併發基礎之正確中止多線程

原理介紹

使用interrupt來通知,而不是強制。java

在JAVA中咱們啓動一個線程很容易的,可是當咱們中止一個線程並非直接馬上立刻就能夠上這個線程中止,JAVA爲咱們提供了interrupt這個方法,簡單來講這個方法的做用就是給當前運行的線程加上一個標誌位,表示當前這個線程能夠中止了,可是這個線程具體何時中止不是咱們可以說了算的,而是由線程自己決定。安全

如何正確中止線程

1)先來看一個一般的狀況,若是一個線程運行到一半,咱們想讓它中止運行,該怎麼作:多線程

/**
 * @author Chen
 * @Description 一般狀況下中止一個多線程
 * @create 2019-11-06 20:45
 */
public class StopThreadWithoutSleep  implements Runnable{
    /**
     * 打印全部10000的倍數,上限是最大整數的一半
     */
    @Override
    public void run() {
        int num = 0;
        while (!Thread.currentThread().isInterrupted()&&num<=Integer.MAX_VALUE/2){
            if (num%10000 == 0){
                System.out.println(num+"是10000的倍數");
            }
            num++;

        }
        System.out.println("任務運行結束了");

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThreadWithoutSleep());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

這裏讓線程休眠1s而後調用了interrupt()告訴線程能夠中止了,而後在線程運行時會進行檢,測若是!Thread.currentThread().isInterrupted()才執行下面的程序,這樣咱們的多線程程序就順利的中止了。dom

2)若是線程阻塞了,會影響咱們中止線程的方式,下面咱們來看一下在阻塞的狀況下線程該如何中止:ide

/**
 * @author Chen
 * @Description 有阻塞的狀況下中止線程
 * @create 2019-11-06 20:58
 */
public class StopThreadWithSleep {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            try {
                int num = 0;
                while (num <= 300 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        System.out.println(num+"是100的倍數");
                    }
                    num++;
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }
}

運行結果:oop

image.png

這個例子是打印300之內100的倍數,打印完以後休眠1000ms,而後在線程運行500ms後中止線程,咱們知道這時候線程應該是在休眠狀態的,在調用這種能然線程阻塞方法時候(如sleep,wait)若是遇到中止線程,此時咱們catch了異常,此時就能夠作到當程序進入阻塞過程當中,依然能夠響應這個中斷。this

3)接下來咱們在來看下一種狀況:若是線程每次循環後都阻塞,這種狀況怎麼解決?線程

/**
 * @author Chen
 * @Description 每次循環都調用sleep或wait等方法,
 * 這種狀況不須要每次迭代都判斷是否已經中斷
 * @create 2019-11-06 20:58
 */
public class StopThreadWithSleepEveryLoop {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            try {
                int num = 0;
                while (num <= 10000) {
                    if (num % 100 == 0) {
                        System.out.println(num+"是100的倍數");
                    }
                    num++;
                    Thread.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

這個例子是打印10000之內100倍數,每次循環後線程都休眠10ms,線程啓動5000ms後進行中斷,此時我咱們能夠思考一下,其實咱們程序在執行每次循環中的方法是很快的,大多數時間都是在休眠10ms的這裏,這時候當咱們調用interrupt(),線程就能夠很快的響應中斷,而不須要咱們每次都在循環開始的時候取判斷中斷標誌位。設計

4)注意:while內放try catch 會致使中斷失效的狀況3d

/**
 * @author Chen
 * @Description while裏面放try/catch會致使 中斷失效的狀況
 * @create 2019-11-06 21:51
 */
public class CannotInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            while (num <=10000 && !Thread.currentThread().isInterrupted()){
                if (num%100== 0){
                    System.out.println(num+"是100的倍數");
                }
                num++;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

運行結果:

image8a962a554c48d23b.png

這個例子和上面的例子是差很少的,一樣是打印10000之內100倍數,每次循環後線程都休眠10ms,線程啓動5000ms後進行中斷,不一樣的是本次把try/catch語句放在了循環裏面,按照咱們上面所學的想象的結果應該是打印出異常信息後線程就中止了,可是實際狀況並非這樣。下面來總結一下緣由:

由於咱們在循環內部已經已經作出了響應,也就是打印出異常信息,此時線程會繼續向下執行,進入下一次循環。而咱們在下一次循環時候也加了判斷標誌位的代碼,依然沒有起到做用,這是由於JAVA在設計sleep的時候有一個理念:當它一旦響應中斷後就會把這個標誌位清除。

5)最佳實踐之優先拋出

/**
 * @author Chen
 * @Description 最佳實踐:catch了InterruptedExcetion以後的優先選擇:在方法簽名中拋出異常 那麼在run()就會強制try/catch
 * @create 2019-11-06 22:23
 */
public class RightWayStopThreadInProd1 implements  Runnable {
    @Override
    public void run() {
        while (true && !Thread.currentThread().isInterrupted()){
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                //保存日誌、中止程序
                System.out.println("保存日誌");
                e.printStackTrace();
            }
        }
    }
    private void throwInMethod() throws InterruptedException {
        //若是此時不是拋出而是try/catch此中斷就會被吞掉
        Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd1());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

這裏咱們須要注意的是若是咱們在run方法中作了不少業務邏輯,而run方法中又調用了其餘的方法。那麼若是其餘的方法中發生了阻塞,而阻塞後若是直接try/catch就會使得咱們的run方法內沒有感知,就不能作相應的處理等。此時run()內部的方法正確的作法應該是把異常向上拋出或者是使用接下來介紹的恢復中斷的方法從新設置標誌位

6)最佳實踐之恢復中斷

/**
 * @author Chen
 * @Description 最佳實踐:在catch子語句中調用Thread.currentThread().interrupt()來恢復設置中斷狀態,
 * 以便於在後續的執行中,依然可以檢查到剛纔發生了中斷
 * @create 2019-11-06 22:23
 */
public class RightWayStopThreadInProd2 implements  Runnable {
    @Override
    public void run() {
        while (true ){
            if (Thread.currentThread().isInterrupted()){
                System.out.println("程序運行結束");
                break;
            }
            System.out.println("go");
            throwInMethod();
        }
    }
    private void catchMethod() {
        //若是此時不是拋出而是try/catch此中斷就會被吞掉
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd2());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

這個例子在run()內調用的方法中try/catch後又從新設置了標誌位,也就是若是在catchMethod()檢測到了遇到中斷會拋出異常進行響應中斷,而後把中斷標誌位恢復,把這個異常捕獲時候咱們作了處理:Thread.currentThread().interrupt()從新設置標誌位後run方法內就可以正常的中斷線程了。

6)可以響應中斷的方法總結:

Object.wait()

Thraed.sleep()

Thread.join()

java.util.concurrent.BlockingQueue.take()/put()

java.util.concurrent.locks.Lock.lockInterruptibly()

java.util.concurrent.CountDownLatch.await()

java.util.concurrent.CyclicBarrier.await()

java.util.concurrent.Exchanger.exchange()

java.nio.channels.InterruptibleChannel相關方法

java.nio.channels.Selector的相關方法

錯誤的中止方法

1)被棄用的stop,suspend和resume方法

使用stop中止線程會使線程立刻中止,不能保證一個單位內全部的任務都執行完成。若是有是一個轉帳操做,而這個操做須要子線程執行多個轉帳,那麼若是子線程執行到一半就中止了會帶來很嚴重的後果。

線程掛起 (suspend)和繼續執行(resume),這兩個 操做是一對相反的操做 ,被掛起的線程,必需要等到resume()操做後,才能繼續執行。不推薦使用suspend(),是由於suspend()在致使線程暫停的同時,並不會去釋聽任何鎖資源。此時,其餘任何線程想要訪問被它暫用的鎖時,都會被牽連,致使沒法正常繼續運行,頗有可能致使死鎖。

2)使用volatile設置boolean設置標記位

volatile能夠簡單的理解爲是解決變量在多個線程之間的可見性問題的一個關鍵字,加了volatile的變量線程均可以訪問到這個變量的值。

下面先來看一下這種方式具體如何使用:

思路就是本身設置一箇中斷標誌位,在循環開始的時候檢查這個標誌位,想要中止線程的時候把中斷標誌位設置爲true。

/**
 * @author Chen
 * @Description 使用Volatile中止多線程例子
 * @create 2019-11-07 18:53
 */
public class UserVolatileInterrupt implements Runnable{
    //設置中斷標記爲false
    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 10;
        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 {
        UserVolatileInterrupt v = new UserVolatileInterrupt();
        Thread thread = new Thread(v);
        thread.start();
        Thread.sleep(5000);
        v.canceled = true;
    }
}

運行結果:

image.png

這種方式看起來可行,但有些時候卻沒法正常中止:下面接着來看一種狀況

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * @author Chen
 * @Description 用volatile不能正常中止的例子
 * @create 2019-11-07 19:07
 */
public class VolatileCannotStop {
    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);
    }
}

/**
 * 生產者
 */
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 + "是10的倍數,被放入到倉庫了。");
                }
                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;
    }
}

imagef6ff7f7ffdb473d6.png

這是一個生產者消費者模式的例子,生產者負責不斷生產消息放入到storage中,而消費者隨機的進行消費數據。一旦不須要生產者生產數據了就把標誌位設置爲true。這種方法看似是可行的,然而運行卻遇到了圖中的狀況,程序並無中止下來。

這是由於生產者生產的速度很快,消費者消費了幾個消息後設置標記位爲true的時候此時生產者的隊列中已經滿了,因此會阻塞在storage.put(num)這裏,從而沒法檢測到標記位的變化。

此時若是使用interrupt方法則不會出現這種狀況。

中止線程相關的方法解析

判斷是否已被中斷的方法:

static boolean interrupted():獲取中斷標誌並重置,目標對象爲當前執行它的線程,與誰調用這個方法無關

boolean isInterrupted():獲取中斷標誌

例子:

/**
 * @author Chen
 * @Description
 * @create 2019-11-07 20:10
 */
public class RightWayInterrupted {

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

        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                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

注意:threadOne.interrupted()Thread.interrupted()做用實際上是同樣的

總結

如何中止線程?

用interrupt來處理,而不是stop或者volatile等方法。把主動權交給被中斷的線程。這樣能夠保證線程的安全中止。

要想達到這樣的效果不只僅須要調用interrupt方法,並且須要被請求方,被中止方和子方法被調用方相互配合。做爲請求方,就是調用Thread.interrupt()發送一箇中斷請求的信號,而被中止方必須在適當的時候去檢查這個中斷信號,而且在可能拋出InterruptedException的時候去處理這個信號。

若是咱們是去寫子方法的,這個子方法須要被線程所調用的,應該注意這時應該優先在字方法拋出Exception,以便其餘人去處理,也能夠進行try/catch後從新設置中斷標誌位。

相關文章
相關標籤/搜索