啟動線程:
從一個最基本的面試題開始,啟動線程到底是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) { } } }
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(); }
結果:最后一行
[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(); }
結果:最后一行
[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(); }
結果: 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(); }
結果: 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(); }
結果:
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(); }
結果和上面一樣,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(); }
結果: 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(); }
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(); }
在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; } }
結果: 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; } } }
結果:
從結果看,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; } } }
結果: 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."); }

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()