您好,登錄后才能下訂單哦!
如何停止線程是Java并發面試中的常見問題,本篇文章將從答題思路到答題細節給出一些參考。
答題思路:
1. 正確方式是中斷
其實從邏輯上也很好理解的,一個線程正在運行,如何讓他停止?
A. 從外部直接調用該線程的stop方法,直接把線程停下來。
B. 從外部通過中斷通知線程停止,然后切換到被停止的線程,該線程執行一系列邏輯后自己停止。
很明顯B方法要比A方法好很多,A方法太暴力了,你根本不知道被停止的線程在執行什么任務就直接把他停止了,程序容易出問題;而B方法把線程停止交給線程本身去做,被停止的線程可以在自己的代碼中進行一些現場保護或者打印錯誤日志等方法再停止,更加合理,程序也更具健壯性。
下面要講的是線程如何能夠響應中斷,第一個方法是通過循環不斷判斷自身是否產生了中斷:
public class Demo1 implements Runnable{ @Override public void run() { int num = 0; while(num <= Integer.MAX_VALUE / 2 && !Thread.currentThread().isInterrupted()){ if(num % 10000 == 0){ System.out.println(num); } num++; } } public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Demo1()); thread.start(); thread.sleep(1000); thread.interrupt(); } }
在上面的代碼中,我們在循環條件中不斷判斷線程本身是否產生了中斷,如果產生了中斷就不再打印
還有一個方法是通過java內定的機制響應中斷:當線程調用sleep(),wait()方法后進入阻塞后,如果線程在阻塞的過程中被中斷了,那么線程會捕獲或拋出一個中斷異常,我們可以根據這個中斷異常去控制線程的停止。具體代碼如下:
public class Demo3 implements Runnable { @Override public void run() { int num = 0; try { while(num < Integer.MAX_VALUE / 2){ if(num % 100 == 0){ System.out.println(num); } num++; Thread.sleep(10); } } catch (InterruptedException e) {//捕獲中斷異常,在本代碼中,出現中斷異常后將退出循環 e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Demo3()); thread.start(); Thread.sleep(5000); thread.interrupt(); } }
2. 各方配合才能完美停止
在上面的兩段代碼中已經可以看到,想通過中斷停止線程是個需要多方配合。上面已經演示了中斷方和被中斷方的配合,下面考慮更多的情況:假如要被停止的線程正在執行某個子方法,這個時候該如何處理中斷?
有兩個辦法:第一個是把中斷傳遞給父方法,第二個是重新設置當前線程為中斷。
先說第一個例子:在子方法中把中斷異常上拋給父方法,然后在父方法中處理中斷:
public class Demo4 implements Runnable{ @Override public void run() { try{//在父方法中捕獲中斷異常 while(true){ System.out.println("go"); throwInterrupt(); } }catch (InterruptedException e) { e.printStackTrace(); System.out.println("檢測到中斷,保存錯誤日志"); } } private void throwInterrupt() throws InterruptedException {//把中斷上傳給父方法 Thread.sleep(2000); } public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Demo4()); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
第二個例子:在子方法中捕獲中斷異常,但是捕獲以后當前線程的中斷控制位將被清除,父方法執行時將無法感知中斷。所以此時在子方法中重新設置中斷,這樣父方法就可以通過對中斷控制位的判斷來處理中斷:
public class Demo5 implements Runnable{ @Override public void run() { while(true && !Thread.currentThread().isInterrupted()){//每次循環判斷中斷控制位 System.out.println("go"); throwInterrupt(); } System.out.println("檢測到了中斷,循環打印退出"); } private void throwInterrupt(){ 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 Demo5()); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
講到這里,正確的停止線程方法已經講的差不多了,下面我們看一下常見的錯誤停止線程的例子:
3. 常見錯誤停止線程例子:
這里介紹兩種常見的錯誤,先說比較好理解的一種,也就是開頭所說的,在外部直接把運行中的線程停止掉。這種暴力的方法很有可能造成臟數據。
看下面的例子:
public class Demo6 implements Runnable{ /** * 模擬指揮軍隊,以一個連隊為單位領取武器,一共有5個連隊,一個連隊10個人 */ @Override public void run() { for(int i = 0; i < 5; i++){ System.out.println("第" + (i + 1) + "個連隊開始領取武器"); for(int j = 0; j < 10; j++){ System.out.println("第" + (j + 1) + "個士兵領取武器"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("第" + (i + 1) + "個連隊領取武器完畢"); } } public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Demo6()); thread.start(); Thread.sleep(2500); thread.stop(); } }
在上面的例子中,我們模擬軍隊發放武器,規定一個連為一個單位,每個連有10個人。當我們直接從外部通過stop方法停止武器發放后。很有可能某個連隊正處于發放武器的過程中,導致部分士兵沒有領到武器。
這就好比在生產環境中,銀行以10筆轉賬為一個單位進行轉賬,如果線程在轉賬的中途被突然停止,那么很可能會造成臟數據。
另外一個“常見”錯誤可能知名度不是太高,就是:通過volatile關鍵字停止線程。具體來說就是通過volatile關鍵字定義一個變量,通過判斷變量來停止線程。這個方法表面上是沒問題的,我們先看這個表面的例子:
public class Demo7 implements Runnable { private static volatile boolean canceled = false; @Override public void run() { int num = 0; while(num <= Integer.MAX_VALUE / 2 && !canceled){ if(num % 100 == 0){ System.out.println(num + "是100的倍數"); } num++; } System.out.println("退出"); } public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Demo7()); thread.start(); Thread.sleep(1000); canceled = true; } }
但是這個方法有一個潛在的大漏洞,就是若線程進入了阻塞狀態,我們將不能通過修改volatile變量來停止線程,看下面的生產者消費者例子:
/** * 通過生產者消費者模式演示volatile的局限性,volatile不能喚醒已經阻塞的線程 * 生產者生產速度很快,消費者消費速度很慢,通過阻塞隊列存儲商品 */ public class Demo8 { 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);//1s足夠讓生產者把阻塞隊列塞滿 Consumer consumer = new Consumer(storage); while(consumer.needMoreNums()){ System.out.println(storage.take() + "被消費"); Thread.sleep(100);//讓消費者消費慢一點,給生產者生產的時間 } System.out.println("消費者消費完畢"); producer.canceled = true;//讓生產者停止生產(實際情況是不行的,因為此時生產者處于阻塞狀態,volatile不能喚醒阻塞狀態的線程) } } class Producer implements Runnable{ public volatile boolean canceled = false; private BlockingQueue storage; public Producer(BlockingQueue storage) { this.storage = storage; } @Override public void run() { int num = 0; try{ while(num < Integer.MAX_VALUE / 2 && !canceled){ if(num % 100 == 0){ this.storage.put(num); System.out.println(num + "是100的倍數,已經被放入倉庫"); } num++; } } catch (InterruptedException e) { e.printStackTrace(); }finally { System.out.println("生產者停止生產"); } } } class Consumer{ private BlockingQueue storage; public Consumer(BlockingQueue storage) { this.storage = storage; } public boolean needMoreNums(){ return Math.random() < 0.95 ? true : false; } }
上面的例子運行后會發現生產線程一直不能停止,因為他處于阻塞狀態,當消費者線程退出后,沒有任何東西能喚醒生產者線程。
這種錯誤用中斷就很好解決:
/** * 通過生產者消費者模式演示volatile的局限性,volatile不能喚醒已經阻塞的線程 * 生產者生產速度很快,消費者消費速度很慢,通過阻塞隊列存儲商品 */ public class Demo8 { 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);//1s足夠讓生產者把阻塞隊列塞滿 Consumer consumer = new Consumer(storage); while(consumer.needMoreNums()){ System.out.println(storage.take() + "被消費"); Thread.sleep(100);//讓消費者消費慢一點,給生產者生產的時間 } System.out.println("消費者消費完畢"); producerThread.interrupt(); } } class Producer implements Runnable{ private BlockingQueue storage; public Producer(BlockingQueue storage) { this.storage = storage; } @Override public void run() { int num = 0; try{ while(num < Integer.MAX_VALUE / 2 && !Thread.currentThread().isInterrupted()){ if(num % 100 == 0){ this.storage.put(num); System.out.println(num + "是100的倍數,已經被放入倉庫"); } num++; } } catch (InterruptedException e) { e.printStackTrace(); }finally { System.out.println("生產者停止生產"); } } } class Consumer{ private BlockingQueue storage; public Consumer(BlockingQueue storage) { this.storage = storage; } public boolean needMoreNums(){ return Math.random() < 0.95 ? true : false; } }
4. 擴展
可能你還會問:如何處理不可中斷的阻塞?
只能說很遺憾沒有一個通用的解決辦法,我們需要針對特定的鎖或io給出特定的解決方案。對于這些特殊的例子,api一般會給出可以響應中斷的操作方法,我們要選用那些特定的方法,沒有萬能藥。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。