1. 程式人生 > >這樣終止執行緒,竟然會導致服務宕機?

這樣終止執行緒,竟然會導致服務宕機?

在開始之前,我們先來看以下程式碼會有什麼問題? ```java public class ThreadStopExample { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { System.out.println("子執行緒開始執行"); // 模擬業務處理 Thread.sleep(1000); } catch (Exception e) { } // 虛擬碼:重要的業務方法 System.out.println("子執行緒的重要業務方法"); }); t1.start(); // 讓子執行緒先執行一點業務 Thread.sleep(100); // 終止子執行緒 t1.stop(); // 等待一段時間,確保子執行緒“執行完” Thread.sleep(3000); System.out.println("主執行緒執行完成"); } } ``` 或許你已經發現了,上面這段程式碼使用了 `Thread.stop()` 來終止執行緒,在 Java 程式中是不允許這樣終止執行緒的。什麼?你問為什麼不能這樣? 首先來說 IDE 都會鄙視你了,它會阻止你使用 `Thread.stop()` ! 什麼?你不信。那麼來看這張圖: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1585830789098-0b01bd07-af1c-4773-b348-0371a2f92486.png#align=left&display=inline&height=145&name=image.png&originHeight=290&originWidth=606&size=35275&status=done&style=none&width=303) 好吧,那為什麼不能這樣用呢?總得給我一個敷衍的理由吧? ## 問題一:破壞了程式的完整性 其實是這樣的,以文章剛開頭的那段程式碼來說,它的執行結果是: > 子執行緒開始執行 > > 主執行緒執行完成  我們發現了一個驚天的大問題,最重要的那段虛擬碼竟然沒執行,如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1585831129379-14e348ea-8dc1-476b-9c05-bc608fc3712b.png#align=left&display=inline&height=206&name=image.png&originHeight=411&originWidth=783&size=52896&status=done&style=none&width=391.5) 可以看出使用 `stop()` 終止執行緒之後,執行緒剩餘的部分程式碼會放棄執行,這樣會造成嚴重的且不易被發現的驚天大 Bug,假如沒有執行的那段程式碼是釋放系統資源的程式碼,或者是此程式的主要邏輯處理程式碼。這就**破壞了程式基本邏輯的完整性,導致意想不到的問題發生**,而且它還很隱祕,不易被發現和修復。 有人說,這還不簡單,我加個 `finally` 不就完了嗎? 這???槓精哪都有,今年特別多。 行,既然這個說服不了你,咱接著往下看。 ## 問題二:破壞了原子邏輯 我們知道在 Java 中 `synchronized` 屬於獨佔式可重入悲觀鎖,如果我們使用它修飾程式碼,妥妥的多執行緒沒問題,但如果碰到 `stop()` 方法就不一定了,直接來看程式碼吧。 ```java public class ThreadStopExample { public static void main(String[] args) throws InterruptedException { MyThread myThread = new MyThread(); Thread t2 = new Thread(myThread); // 開啟執行緒 t2.start(); for (int i = 0; i < 10; i++) { Thread t = new Thread(myThread); t.start(); } // 結束執行緒 t2.stop(); } /** * 自定義原子測試執行緒 */ static class MyThread implements Runnable { // 計數器 int num = 0; @Override public void run() { // 同步程式碼塊,保證原子操作 synchronized (MyThread.class) { // 自增 num++; try { // 執行緒休眠 0.1 秒 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 自減 num--; System.out.println(Thread.currentThread().getName() + " | num=" + num); } } } } ``` 以上程式的執行結果為: >
Thread-5 | num=1 > > Thread-4 | num=1 > > Thread-2 | num=1 > > Thread-1 | num=1 > > Thread-8 | num=1 > > Thread-6 | num=1 > > Thread-9 | num=1 > > Thread-3 | num=1 > > Thread-7 | num=1 > > Thread-10 | num=1 > 從結果可以看出,以上程式碼經過 `synchronized` 修飾的 ++ 和 -- 操作,到最後列印的結果 num 竟然不是 0,而是 1。 這是**因為 `stop()` 方法會釋放此執行緒中的所有鎖,導致程式執行紊亂,破壞了程式的原子操作邏輯**。 以上的這些問題,導致了 JDK 廢棄了 `stop()` 的方法,它的廢棄原始碼如下: ```java /** * Forces the thread to stop executing. *

* If there is a security manager installed, its checkAccess

* method is called with this * as its argument. This may result in a * SecurityException being raised (in the current thread). *

* If this thread is different from the current thread (that is, the current * thread is trying to stop a thread other than itself), the * security manager's checkPermission

method (with a * RuntimePermission("stopThread") argument) is called in * addition. * Again, this may result in throwing a * SecurityException (in the current thread). *

* The thread represented by this thread is forced to stop whatever * it is doing abnormally and to throw a newly created * ThreadDeath object as an exception. *

* It is permitted to stop a thread that has not yet been started. * If the thread is eventually started, it immediately terminates. *

* An application should not normally try to catch * ThreadDeath unless it must do some extraordinary * cleanup operation (note that the throwing of * ThreadDeath causes finally clauses of * try statements to be executed before the thread * officially dies). If a catch clause catches a * ThreadDeath object, it is important to rethrow the * object so that the thread actually dies. *

* The top-level error handler that reacts to otherwise uncaught * exceptions does not print out a message or otherwise notify the * application if the uncaught exception is an instance of * ThreadDeath. * * @exception SecurityException if the current thread cannot * modify this thread. * @see #interrupt() * @see #checkAccess() * @see #run() * @see #start() * @see ThreadDeath * @see ThreadGroup#uncaughtException(Thread,Throwable) * @see SecurityManager#checkAccess(Thread) * @see SecurityManager#checkPermission * @deprecated This method is inherently unsafe. Stopping a thread with * Thread.stop causes it to unlock all of the monitors that it * has locked (as a natural consequence of the unchecked * ThreadDeath exception propagating up the stack). If * any of the objects previously protected by these monitors were in * an inconsistent state, the damaged objects become visible to * other threads, potentially resulting in arbitrary behavior. Many * uses of stop should be replaced by code that simply * modifies some variable to indicate that the target thread should * stop running. The target thread should check this variable * regularly, and return from its run method in an orderly fashion * if the variable indicates that it is to stop running. If the * target thread waits for long periods (on a condition variable, * for example), the interrupt method should be used to * interrupt the wait. * For more information, see * Why * are Thread.stop, Thread.suspend and Thread.resume Deprecated?. */ @Deprecated public final void stop() { SecurityManager security = System.getSecurityManager(); if (security != null) { checkAccess(); if (this != Thread.currentThread()) { security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION); } } // A zero status value corresponds to "NEW", it can't change to // not-NEW because we hold the lock. if (threadStatus != 0) { resume(); // Wake up thread if it was suspended; no-op otherwise } // The VM can handle all thread states stop0(new ThreadDeath()); } ``` 可以看出 `stop()` 方法被 `@Deprecated` 註釋修飾了,而被此註解修飾的程式碼表示為過時方法,不建議被使用。從 `stop()` 的備註資訊可以看出,官方也不建議使用 `stop()` ,說它是一個非安全的方法。 ## 正確終止執行緒 那如何終止執行緒呢?這裡提供 2 個正確的方法: 1. 設定退出標識退出執行緒; 1. 使用 `interrupt()` 方法終止執行緒。 ### 1.自定義退出標識 我們可以自定義一個布林變數來標識是否需要退出執行緒,實現程式碼如下: ```java // 自定義退出標識退出執行緒 static class FlagThread extends Thread { public volatile boolean exit = false; public void run() { while (!exit) { // 執行正常的業務邏輯 } } } ``` 可以看出我們使用了關鍵字 `volatile` 對執行緒進行了修飾,這樣就可以保證多執行緒的執行安全了,在我們需要讓執行緒退出時,只需要把變數 `exit` 賦值為 `true` 就可以了。 ### 2.interrupt 終止執行緒 當我們使用 `interrupt()` 方法時,以上兩個示例的執行結果就正常了,執行程式碼如下: ```java public class ThreadStopExample { public static void main(String[] args) throws InterruptedException { // 問題一:破壞了程式的完整性 Thread t1 = new Thread(() -> { try { System.out.println("子執行緒開始執行"); // 模擬業務處理 Thread.sleep(1000); } catch (Exception e) { } // 虛擬碼:重要業務方法 System.out.println("子執行緒的重要業務方法"); }); t1.start(); // 讓子執行緒先執行一點業務 Thread.sleep(100); // 終止子執行緒 t1.interrupt(); // 等待一段時間,確保子執行緒“執行完” Thread.sleep(3000); System.out.println("主執行緒執行完成"); // 問題二:破壞了原子邏輯 MyThread myThread = new MyThread(); Thread t2 = new Thread(myThread); // 開啟執行緒 t2.start(); for (int i = 0; i < 10; i++) { Thread t = new Thread(myThread); t.start(); } // 結束執行緒 t2.interrupt(); } /** * 自定義原子測試執行緒 */ static class MyThread implements Runnable { // 計數器 int num = 0; @Override public void run() { // 同步程式碼塊,保證原子操作 synchronized (MyThread.class) { // 自增 num++; try { // 執行緒休眠 0.1 秒 Thread.sleep(100); } catch (InterruptedException e) { System.out.println(e.getMessage()); } // 自減 num--; System.out.println(Thread.currentThread().getName() + " | num=" + num); } } } } ``` 以上程式的執行結果為: > 子執行緒開始執行 > > 子執行緒的重要業務方法 > > 主執行緒執行完成 > > sleep interrupted > > Thread-1 | num=0 > > Thread-9 | num=0 > > Thread-10 | num=0 > > Thread-7 | num=0 > > Thread-6 | num=0 > > Thread-5 | num=0 > > Thread-4 | num=0 > > Thread-2 | num=0 > > Thread-3 | num=0 > > Thread-11 | num=0 > > Thread-8 | num=0 > 可以看出以上的執行都符合我們的預期,這才是正確的終止執行緒的方式。 ## 總結 本文我們講了執行緒的三種終止方式,自定義退出標識的方式、使用 `stop()` 的方式或 `interrupt()` 的方式。其中 `stop()` 的方式會導致程式的完整性和原子性被破壞的問題,並且此方法被 JDK 標識為過期方法,不建議使用,而 `interrupt()` 方法無疑是最適合我們的終止執行緒的