並發和多線程(二)--啟動和中斷線程(Interrupt)的正確姿勢


啟動線程:

  從一個最基本的面試題開始,啟動線程到底是start()還是run()?

Runnable runnable = () -> System.out.println(Thread.currentThread().getName());
Thread thread = new Thread(runnable);
thread.run();
thread.start();
結果:
main
Thread-0

  我們可以看到thread.run()是通過main線程執行的,而start()啟動的才是一個新線程。run()只是在線程啟動的時候進行回調而已,如果沒有start(),run()也只是一個普通方法。

  start()方法不一定直接啟動新線程,而是請求jvm在空閑的時候去啟動,由線程調度器決定。

思考題:如果重復執行start()方法會怎樣?

Runnable runnable = () -> System.out.println(Thread.currentThread().getName());
Thread thread = new Thread(runnable);
thread.start();
thread.start();
結果:
Exception in thread "main" Thread-0
java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:705)
at com.diamondshine.Thread.ThreadClass.main(ThreadClass.java:33)

重復執行start()會出現異常,可以從start()的源碼得到,因為啟動線程的時候,會檢測當前線程狀態

public synchronized void start() {
    
    if (threadStatus != 0)    //判斷線程啟動時的狀態是否為new,如果不是,直接拋出異常
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {

        }
    }
}
View Code

PS:關於run()對應Thread和Runnable的區別,請參考上一篇博客並發和多線程(一)--創建線程的方式

線程停止

  相比線程啟動,線程停止要復雜很多,有些方式雖然可以正常使用,但是可能存在某些風險,也是我們需要深入學習的地方。

1、stop、suspend、resume

  這三種方式一般來說不會對我們有影響,因為是已廢棄的方法,官方不推薦使用

stop():

  廢棄的原因,可以查看:https://docs.oracle.com/javase/7/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

PS:stop執行之后,會釋放monitor的鎖,而不是像某些並發書里面寫的不釋放鎖,參考上面的文檔。

suspend和resume:

  是配套使用的,suspend本身不釋放鎖進行休眠,等待resume喚醒,這樣很容易滿足死鎖的條件,所以官方不推薦使用。

2、Interrupt()

  我們中斷一個線程通常使用Interrupt(),官方廢棄stop(),推薦的也是通過Interrupt()實現線程中斷。Interrupt()的特點是通知中斷線程,而這個線程是否中斷選擇權在於其本身,這是官方開發人員設計思想:需要被停止的線程可能不是你寫的,對其了解可能不夠,所以講是否中斷的選擇權交於其本身,Interrupt()只是改變中斷位的狀態。

  中斷線程的代碼書寫,取決於線程運行的方式,所以我們可以分類進行分析處理。

2.1).一般情況

  一般情況是指,沒有調用sleep()、wait()等阻塞狀態下的中斷。

public static void main(String[] args){
    Runnable runnable = () -> {
        int num = 0;
        while (num <= Integer.MAX_VALUE / 10) {
            if (num % 1000 == 0) {
                log.info("{}為1000的倍數", num);
            }
            num++;
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread.interrupt();
}
View Code
結果:最后一行
[Thread-0] INFO com.diamondshine.Thread.ThreadClass - 214748000為1000的倍數

  從結果上看,interrupt()並沒有 讓線程停止,因為Integer的最大值2147483647

 正確的代碼應該是:

public static void main(String[] args){
    Runnable runnable = () -> {
        int num = 0;
        while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 10) {
            if (num % 1000 == 0) {
                log.info("{}為1000的倍數", num);
            }
            num++;
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread.interrupt();
}
View Code
結果:最后一行
[Thread-0] INFO com.diamondshine.Thread.ThreadClass - 96540000為1000的倍數

   通過結果知道,成功中斷了線程。通過isInterrupted()的判斷讓線程提前中斷了,所以interrupt()的作用只是將Interrupted標志位置為true。

2.2).線程處於阻塞狀態

public static void main(String[] args) throws InterruptedException{
    Runnable runnable = () -> {
        int num = 0;
        try {
            while (!Thread.currentThread().isInterrupted() && num <= 100) {
                if (num % 100 == 0) {
                    log.info("{}為100的倍數", num);
                }
                num++;
            }
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(5000);
    thread.interrupt();
}
View Code
結果:
16:24:44.403 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 942000為1000的倍數
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at com.diamondshine.Thread.ThreadClass.lambda$main$0(ThreadClass.java:28)
    at java.lang.Thread.run(Thread.java:745)

  主線程sleep休眠5000ms,然后執行interrupt(),而Thread-0只是執行代碼時間很短,然后進入sleep,最后出現異常,程序停止運行。所以處於sleep阻塞狀態下,可以自動通過拋出異常來相應interrupt(),然后try catch進行捕獲異常。

 2.3).線程每次迭代都進行阻塞

public static void main(String[] args) throws InterruptedException{
    Runnable runnable = () -> {
        int num = 0;
        try {
            while (num <= Integer.MAX_VALUE) {
                if (num % 100 == 0) {
                    log.info("{}為100的倍數", num);
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(500);
    thread.interrupt();
}
View Code
結果:
20:26:15.870 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 0為100的倍數
20:26:16.060 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 100為100的倍數
20:26:16.254 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 200為100的倍數
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at com.diamondshine.Thread.ThreadClass.lambda$main$0(ThreadClass.java:24)
    at java.lang.Thread.run(Thread.java:745)

  從上面代碼可以看到,在每次迭代都阻塞的場景下,即使沒有使用isInterrupted()判斷,通過sleep對interrupt()方法做出響應,也可以實現線程中斷。

sleep自動清除中斷信號

public static void main(String[] args) throws InterruptedException{
    Runnable runnable = () -> {
        int num = 0;
            while (num <= Integer.MAX_VALUE) {
                if (num % 100 == 0) {
                    log.info("{}為100的倍數", num);
                }
                num++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(500);
    thread.interrupt();
}
View Code
結果:
20:34:40.499 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 100為100的倍數 20:34:40.701 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 200為100的倍數 java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at com.diamondshine.Thread.ThreadClass.lambda$main$0(ThreadClass.java:24) at java.lang.Thread.run(Thread.java:745) 20:34:40.897 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 300為100的倍數 20:34:41.091 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 400為100的倍數 20:34:41.283 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 500為100的倍數

  我們改變了try catch的位置,發現sleep響應interrupt拋出異常之后,程序繼續執行。這是因為拋出的異常被catch住了,但是while循環還是繼續的。這種情況下怎么解決呢?嘗試在while循環加上對interrupt標志位判斷。

public static void main(String[] args) throws InterruptedException{
    Runnable runnable = () -> {
        int num = 0;
            while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE) {
                if (num % 100 == 0) {
                    log.info("{}為100的倍數", num);
                }
                num++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(500);
    thread.interrupt();
}
View Code

  結果和上面一樣,sleep響應interrupt,但是程序還是繼續執行,原因就是因為當線程在sleep過程中相應中斷,會自動清除中斷標志位,所以這是一種錯誤的寫法。

線程中斷的最佳實踐

  在項目開發中,一般不會像上面直接在Runnable中通過lamdba寫邏輯,因為邏輯沒有這么簡單,可能會在run()中調用方法,這時候就需要注意代碼的書寫,一般有兩種方式。

1、傳遞中斷信息

錯誤的方式:

private static void doSomething() {
    try {
        //do something
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) throws InterruptedException{
    Runnable runnable = () -> {
            while (true) {
                //do something
                System.out.println("continue");
                doSomething();
            }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(500);
    thread.interrupt();
}
View Code
結果:
continue
continue
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at com.diamondshine.Thread.ThreadClass.doSomething(ThreadClass.java:18)
    at com.diamondshine.Thread.ThreadClass.lambda$main$0(ThreadClass.java:28)
    at java.lang.Thread.run(Thread.java:745)
continue
continue 

  這種方式,在方法內部吃掉異常,導致外層調用這個方法,無法終止線程。

正確寫法:throws向上拋出異常,將interrupt傳遞出去。

正確做法:
private static void doSomething() throws InterruptedException{
    //do something
    Thread.sleep(1000);
}

public static void main(String[] args) throws InterruptedException{
    Runnable runnable = () -> {
        try {
            while (true) {
                //do something
                System.out.println("continue");
                doSomething();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(500);
    thread.interrupt();
}
View Code

2、恢復中斷

 如果不想傳遞中斷,就只能采用這種方式,代碼如下。

private static void doSomething(){
    try {
        //do something
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    //在sleep響應中斷時候,重新恢復中斷信息
        Thread.currentThread().interrupt();
        e.printStackTrace();
    }
}

public static void main(String[] args) throws InterruptedException{
    Runnable runnable = () -> {
        int num = 0;
        while (num <= 10000) {
            if (!Thread.currentThread().isInterrupted()) {
                //do something
                System.out.println("continue");
                break;
            }
            doSomething();
            num++;
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(500);
    thread.interrupt();
}
View Code

在catch代碼塊中重新恢復中斷信息,然后通過 isInterrupted()判斷狀態,然后break跳出循環。

我們前面演示了sleep響應中斷,除了sleep(),還有很多方法可以做到:

wait()、join()、BlockingQueue.take()/put()、Lock.lockInterruptibly()、CountdownLatch、CyclicBarrier、Exchange、nio。

使用Interrupt的好處:

1、使用interrupt中斷線程不是強制性的,相對比較安全。

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

2.1).請求方:

  發出中斷信號。

2.2).被停止方:

  在合適的時候檢查中斷信號,並且可能拋出InterrupedException的地方處理該中斷信號。

2.3).子方法被調用方:

  優先在方法層面拋出InterrupedException,或者檢查到中斷信號時,再次設置中斷狀態。

3、volatile實現線程中斷

@Slf4j
public class ThreadClass implements Runnable{

    //創建volatile修飾的變量
    private volatile boolean flag = false;

    @Override
    public void run() {
        int num = 0;
        try {
            while (!flag && num <= 10000) {    //通過volatile的可見性實現線程中斷
                if (num % 100 == 0) {
                    log.info("{}為100的倍數", num);
                }
                Thread.sleep(1);
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException{
        ThreadClass threadClass = new ThreadClass();
        Thread thread = new Thread(threadClass);
        thread.start();
        Thread.sleep(500);
        threadClass.flag = true;
    }

}
View Code
結果:
13:47:14.660 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 0為100的倍數
13:47:14.858 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 100為100的倍數
13:47:15.027 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 200為100的倍數

  從上面結果看,通過volatile的可見性同樣實現了線程中斷(關於可見性,可以查看JMM和線程安全三大特性相關內容)

volatile實現線程中斷的限制

  長時間阻塞的場景下,volatile是無法中斷線程的,例如使用wait()或者阻塞隊列。如果現在有個生產者消費者場景,生產者生產的很快,但是消費者消費速度不夠,如果Consumer不需要更多的,請求Producer終止線程,這個場景應該是很有可能出現的。

@Slf4j
public class ThreadClass{

    public static void main(String[] args) throws InterruptedException{
        ThreadClass threadClass = new ThreadClass();
        ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(10);
        Producer producer = threadClass.new Producer(queue);
        Thread thread = new Thread(producer);
        thread.start();
        Thread.sleep(1000);

        Consumer consumer = threadClass.new Consumer(queue);
        while (consumer.needMoreNums()) {   //通過這個方法模擬需要進行Producer限流
            log.info("{}被消費了", consumer.blockingQueue.take());
            Thread.sleep(100);  //每次消費sleep 100ms,模擬Consumer消費慢的場景
        }
        System.out.println("消費者不需要更多數據了。");

        //一旦消費不需要更多數據了,我們應該讓生產者也停下來,但是實際情況
        producer.flag=true;
        log.info("{}", producer.flag);
//        thread.interrupt();
    }

    class Producer implements Runnable{

        private volatile boolean flag = false;

        private ArrayBlockingQueue blockingQueue;

        public Producer(ArrayBlockingQueue blockingQueue) {
            this.blockingQueue = blockingQueue;
        }

        @Override
        public void run() {
            int num = 0;
            try {
                while (!flag && num <= 10000) {
                    if (num % 100 == 0) {
                        blockingQueue.put(num); //最終Thread-0會被阻塞在這里
                        log.info("{}被放入生產者隊列", num);
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                log.info("生產者運行結束");
            }
        }
    }
    class Consumer{

        private ArrayBlockingQueue blockingQueue;

        public Consumer(ArrayBlockingQueue blockingQueue) {
            this.blockingQueue = blockingQueue;
        }

        //通過needMoreNums模擬消費者限流的情況
        public boolean needMoreNums() {
            if (Math.random() > 0.95) {
                return false;
            }
            return true;
        }
    }
}
生產者-消費者場景-volatile中斷線程

結果:

   從結果看,Consumer消費太慢,導致Producer阻塞在blockingQueue.put(),這時候無法通過while檢測狀態,但是interrupt就可以解決這個場景。

@Slf4j
public class ThreadClass{

    public static void main(String[] args) throws InterruptedException{
        ThreadClass threadClass = new ThreadClass();
        ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(10);
        Producer producer = threadClass.new Producer(queue);
        Thread thread = new Thread(producer);
        thread.start();
        Thread.sleep(1000);

        Consumer consumer = threadClass.new Consumer(queue);
        while (consumer.needMoreNums()) {   //通過這個方法模擬需要進行Producer限流
            log.info("{}被消費了", consumer.blockingQueue.take());
            Thread.sleep(100);  //每次消費sleep 100ms,模擬Consumer消費慢的場景
        }
        System.out.println("消費者不需要更多數據了。");

        //一旦消費不需要更多數據了,我們應該讓生產者也停下來,但是實際情況
        thread.interrupt(); //通過interrupt中斷線程
    }

    class Producer implements Runnable{

        private volatile boolean flag = false;

        private ArrayBlockingQueue blockingQueue;

        public Producer(ArrayBlockingQueue blockingQueue) {
            this.blockingQueue = blockingQueue;
        }

        @Override
        public void run() {
            int num = 0;
            try {
                while (!Thread.currentThread().isInterrupted() && num <= 10000) {
                    if (num % 100 == 0) {
                        blockingQueue.put(num); //最終Thread-0會被阻塞在這里
                        log.info("{}被放入生產者隊列", num);
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                log.info("生產者運行結束");
            }
        }
    }
    class Consumer{

        private ArrayBlockingQueue blockingQueue;

        public Consumer(ArrayBlockingQueue blockingQueue) {
            this.blockingQueue = blockingQueue;
        }

        //通過needMoreNums模擬消費者限流的情況
        public boolean needMoreNums() {
            if (Math.random() > 0.95) {
                return false;
            }
            return true;
        }
    }
}
生產者-消費者-interrupt中斷線程
結果:
14:56:11.562 [main] INFO com.diamondshine.Thread.ThreadClass - 2100被消費了
14:56:11.562 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 3100被放入生產者隊列
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.diamondshine.Thread.ThreadClass$Producer.run(ThreadClass.java:52)
    at java.lang.Thread.run(Thread.java:745)
消費者不需要更多數據了。
14:56:11.663 [Thread-0] INFO com.diamondshine.Thread.ThreadClass - 生產者運行結束

  jvm設計人員考慮到長時間阻塞的場景,通過interrupt照樣可以解決,拋出異常,相應阻塞。這里Thread.currentThread().isInterrupted()替代flag,沒有也是可以的,因為是通過拋出異常響應的中斷。

總結:

  如果我們遇到了線程長時間阻塞(很常見的情況),volatile就沒辦法及時喚醒它,或者永遠都無法喚醒該線程,而interrupt設計之初就是把wait等長期阻塞作為一種特殊情況考慮在內了,我們應該用interrupt思維來停止線程。

  interrupt對應的native方法,Thread.sleep() 、lockSupport.park()、 Synchronized同步塊、Object.wait()等都可以做出中斷相應。

判斷中斷狀態:

 interrupted():

  static方法,只關注哪個類執行interrupted()這行代碼,而和*.interrupted()的這個*沒有關系,返回Interrupt狀態,但是會把狀態位ClearInterrupted置為false,自動清除狀態。

isInterruped():

  返回Interrupt狀態。

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

    // 啟動線程
    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());

    System.out.println("Main thread is over.");
}
View Code
isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true
Main thread is over.
結果

  先思考一下,再看一下結果,和你想的是否一致,如果真的理解了這兩個方法,這段代碼運行結果應該很容易理解。

思考題:

如何處理不可中斷阻塞?
A. 用interrupt方法來請求停止線程 
B. 不可中斷的阻塞無法處理 
C. 根據不同的類調用不同的方法
答案:C

  如果線程阻塞是由於調用了 wait(),sleep() 或 join() 方法,可以中斷線程,通過拋出InterruptedException異常來喚醒該線程相應阻塞。 對於不能響應InterruptedException的阻塞,並沒有一個通用的解決方案。 但是我們可以利用特定的其它的可以響應中斷的方法,比如ReentrantLock.lockInterruptibly,比如關閉套接字使線程立即返回等方法來達到目的。因為有很多原因會造成線程阻塞,所以針對不同情況,喚起的方法也不同。

總結:

interrupt這部分內容比較多吧,如果感覺有幫助,需要多想想,下面是基本總結,如果看着這些能想到對應的內容,那說明掌握的不錯。

啟動線程start()和run()

多次執行start()的結果

停止線程

1、stop、suspend、resume 不推薦

2、interrupt()

多種情況下的中斷,一般場景,阻塞狀態下,每次遍歷都阻塞

sleep、wait等阻塞狀態下,對interrupt的響應,而且會直接清除中斷信號

最佳實踐

1、傳遞中斷

2、恢復中斷

3、volatile實現

一般場景可以實現

長時間阻塞,volatile無法實現中斷,interrupt可以滿足這個場景

判斷線程中斷的兩個方法

interrupted()

isInterruped()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM