在開始之前,我們先來看以下代碼會有什么問題?
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()
!
什么?你不信。那么來看這張圖:
好吧,那為什么不能這樣用呢?總得給我一個敷衍的理由吧?
問題一:破壞了程序的完整性
其實是這樣的,以文章剛開頭的那段代碼來說,它的執行結果是:
子線程開始執行
主線程執行完成
我們發現了一個驚天的大問題,最重要的那段偽代碼竟然沒執行,如下圖所示:
可以看出使用 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 個正確的方法:
- 設置退出標識退出線程;
- 使用
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()
方法無疑是最適合我們的終止線程的方式。