­

這樣終止線程,竟然會導致服務宕機?

在開始之前,我們先來看以下代碼會有什麼問題?

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

好吧,那為什麼不能這樣用呢?總得給我一個敷衍的理由吧?

問題一:破壞了程序的完整性

其實是這樣的,以文章剛開頭的那段代碼來說,它的執行結果是:

子線程開始執行

主線程執行完成

我們發現了一個驚天的大問題,最重要的那段偽代碼竟然沒執行,如下圖所示:
image.png

可以看出使用 stop() 終止線程之後,線程剩餘的部分代碼會放棄執行,這樣會造成嚴重的且不易被發現的驚天大 Bug,假如沒有執行的那段代碼是釋放系統資源的代碼,或者是此程序的主要邏輯處理代碼。這就破壞了程序基本邏輯的完整性,導致意想不到的問題發生,而且它還很隱秘,不易被發現和修復。

有人說,這還不簡單,我加個 finally 不就完了嗎?

這???杠精哪都有,今年特別多。

行,既然這個說服不了你,咱接着往下看。

問題二:破壞了原子邏輯

我們知道在 Java 中 synchronized 屬於獨佔式可重入悲觀鎖,如果我們使用它修飾代碼,妥妥的多線程沒問題,但如果碰到 stop() 方法就不一定了,直接來看代碼吧。

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() 的方法,它的廢棄源碼如下:

/**   * Forces the thread to stop executing.   * <p>   * If there is a security manager installed, its <code>checkAccess</code>   * method is called with <code>this</code>   * as its argument. This may result in a   * <code>SecurityException</code> being raised (in the current thread).   * <p>   * 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 <code>checkPermission</code> method (with a   * <code>RuntimePermission("stopThread")</code> argument) is called in   * addition.   * Again, this may result in throwing a   * <code>SecurityException</code> (in the current thread).   * <p>   * The thread represented by this thread is forced to stop whatever   * it is doing abnormally and to throw a newly created   * <code>ThreadDeath</code> object as an exception.   * <p>   * It is permitted to stop a thread that has not yet been started.   * If the thread is eventually started, it immediately terminates.   * <p>   * An application should not normally try to catch   * <code>ThreadDeath</code> unless it must do some extraordinary   * cleanup operation (note that the throwing of   * <code>ThreadDeath</code> causes <code>finally</code> clauses of   * <code>try</code> statements to be executed before the thread   * officially dies).  If a <code>catch</code> clause catches a   * <code>ThreadDeath</code> object, it is important to rethrow the   * object so that the thread actually dies.   * <p>   * 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   * <code>ThreadDeath</code>.   *   * @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   *       <code>ThreadDeath</code> 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 <code>stop</code> 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 <code>interrupt</code> method should be used to   *       interrupt the wait.   *       For more information, see   *       <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">Why   *       are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.   */  @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. 設置退出標識退出線程;
  2. 使用 interrupt() 方法終止線程。

1.自定義退出標識

我們可以自定義一個布爾變量來標識是否需要退出線程,實現代碼如下:

// 自定義退出標識退出線程  static class FlagThread extends Thread {      public volatile boolean exit = false;        public void run() {          while (!exit) {              // 執行正常的業務邏輯          }      }  }  

可以看出我們使用了關鍵字 volatile 對線程進行了修飾,這樣就可以保證多線程的執行安全了,在我們需要讓線程退出時,只需要把變量 exit 賦值為 true 就可以了。

2.interrupt 終止線程

當我們使用 interrupt() 方法時,以上兩個示例的執行結果就正常了,執行代碼如下:

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() 方法無疑是最適合我們的終止線程的方式。