如何优雅地关闭一个线程
前面给大家展示了如何创建和使用Java线程,讲解了线程的状态及其每个状态之间的流转,接下来给大家分享,如何正确地关闭一个Java线程。
我们知道,一个Java线程被启动后,线程类中的run()
方法会被回调,当run()
方法执行完成后进入终止状态,这是线程的自然终止。当我们不想让线程自然终止,想在其运行期间就关闭,应该怎么做呢?
Thread.stop()
在Thread
中提供了一个stop()
方法,用于关闭正在运行中的线程。大家可以去看看这个方法的源码,可以看到它被@Deprecated
注解标识,意味着此方法是不推荐使用的。大家可以看看文档说明👇
https://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html
简单说明一下,该方法会导致两个问题
-
在执行run方法时,每一个行代码都可能会抛出 ThreadDeath 异常 -
会释放当前线程中的所有锁,释放之后可能会让一些操作失去原子性。
下面可以看看示例代码
public class CloseThreadEample implements Runnable {
@Override
public void run() {
try {
for (int i = 0; i < 1000 ;i++ ) {
System.out.println("HelloWorld" + i);
}
System.out.println("over");
} catch (Throwable e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new CloseThreadEample());
thread.start();
Thread.sleep(1);
thread.stop();
}
}
运行结果:
HelloWorld52
HelloWorld53java.lang.ThreadDeath
at java.lang.Thread.stop(Thread.java:853)
at org.example.closethread.CloseThreadEample.main(CloseThreadEample.java:25)
这段运行结果已经展示了前面提到的两个问题,第一个问题可以明显看到,使用stop
后,会抛出ThreadDeath
异常,让run
方法被中断,从而无法正常打印出 “over”,那第二个问题是怎么体现出来的呢?我们可以继续看看System.out.println
方法的源码
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
println
方法是使用了synchronized
关键字,理论上print(x)
和newLine()
两行指令是原子性的,也就是说要么一起执行成功,要么都不执行,从运行结果可以看到,打印完“HelloWorld53”后,没有继续打印换行操作,就直接打印了异常信息,这就说明了,使用stop
方法后,释放了synchronized
同步锁,使得用于打印的两行指令代码失去了原子性,从而导致了newLine()
没有执行。既然stop()
方法不推荐使用,那还有什么方法关闭线程呢?
Thread.interrupt()
Thread
提供了interrupt()
方法,它没有像stop()
方法那样,直接中断一个线程的执行,而是通过信号量来通知线程可以停止执行了,当线程收到该信号量时,可以通过isInterrupted()
方法判断当前是否需要中断。这样的停止方式,相比stop()
是不是更加友好,让线程自己判断能否中断,从而保证业务处理的完整性。
下面看看具体例子:
public class InterruptThreadExample implements Runnable {
@Override
public void run() {
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println("helloworld" + i++);
}
System.out.println("over");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptThreadExample());
thread.start();
thread.sleep(10);
thread.interrupt();
}
}
运行结果:
helloworld260
helloworld261
helloworld262
over
在示例代码中,通过isInterrupted()
方法判断线程是否被中断,如果不是,则循环打印“helloworld”字符串。可以看到,使用interrupt()
方法后,线程是正常结束的,“over” 也是正常打印。
当然,虽然这种方式可以友好地停止线程,但是,如果使用不当,线程是无法被停止的。
当一个线程因为sleep()
,Object.wait()
等方法进入阻塞状态时,此时调用interrupt()
方法,线程是无法正常响应的,它会抛出一个InterruptedException
异常,这个异常我们可以在各个阻塞方法中看到。
public static void sleep(long millis, int nanos) throws InterruptedException {...}
public static native void sleep(long millis) throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
public final void join() throws InterruptedException {...}
...
抛出这个异常有什么用呢?下面我们继续看例子
public class InterruptThreadExample implements Runnable {
@Override
public void run() {
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println("helloworld" + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("over");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptThreadExample());
thread.start();
thread.sleep(1000);
thread.interrupt();
}
}
运行结果:
helloworld6
helloworld7
helloworld8
helloworld9
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at org.example.closethread.InterruptThreadExample.run(InterruptThreadExample.java:14)
at java.lang.Thread.run(Thread.java:748)
helloworld10
helloworld11
helloworld12
笔者在原来的代码上继续加上了Thread.sleep(100)
代码,从运行结果可以看到,当调用interrupt()
方法后,会抛出异常,但是线程仍然在运行,持续打印“helloworld”,这是什么原因造成的呢?笔者也不卖关子了,继续看下一个例子。
public class InterruptThreadExample implements Runnable {
@Override
public void run() {
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println("helloworld" + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("出现异常后线程的中断状态: " + Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
System.out.println("重新调用interrupt()方法后的线程状态:" + Thread.currentThread().isInterrupted());
e.printStackTrace();
}
}
System.out.println("over");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptThreadExample());
thread.start();
thread.sleep(1000);
thread.interrupt();
System.out.println("调用interrpt()方法后的线程状态:" + thread.isInterrupted());
}
}
运行结果:
helloworld8
helloworld9
调用interrpt()方法后的线程状态:true
出现异常后线程的中断状态: false
重新调用interrupt()方法后的线程状态:true
over
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at org.example.closethread.InterruptThreadExample.run(InterruptThreadExample.java:14)
at java.lang.Thread.run(Thread.java:748)
在这个例子中,笔者分别在调用interrupt()
方法后和出现异常后打印了线程的isInterrupted()
信息,从运行结果可以看到,线程是被正常关闭的,一开始在main方法中调用了interrupt()
方法后,可以看到isInterrupted()
是为ture
的,但是在出现异常之后,isInterrupted()
又变回了false
,这是因为在抛出异常时,除了唤醒被阻塞的线程外,还会对中断标记进行复位,将中断标记重新设置为false
,此时需要重新调用interrupt()
方法才能中断线程。
这样设计的目的是为了将中断决定权交给线程本身,如果抛出异常,提醒当前线程有中断的操作,至于接下来怎么处理,就取决于线程本身如何决定了,可以不用理会此次中断操作继续向下执行,也可以通过break
中断死循环,或者通过return
直接返回等等。
我们可以看看interrupt()
方法的源码
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
private native void interrupt0();
可以看到, 在该方法中,通过调用interrupt0()
方法设置一个标记,而此方法又是native方法,它是使用C/C++语言实现的接口,设置标记的逻辑是在JVM层面实现的。
接下来继续看看 isInterrupted()
方法的源码
/**
* Tests whether the current thread has been interrupted. The
* <i>interrupted status</i> of the thread is cleared by this method. In
* other words, if this method were to be called twice in succession, the
* second call would return false (unless the current thread were
* interrupted again, after the first call had cleared its interrupted
* status and before the second call had examined it).
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if the current thread has been interrupted;
* <code>false</code> otherwise.
* @see #isInterrupted()
* @revised 6.0
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* Tests whether this thread has been interrupted. The <i>interrupted
* status</i> of the thread is unaffected by this method.
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if this thread has been interrupted;
* <code>false</code> otherwise.
* @see #interrupted()
* @revised 6.0
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
同样,最终也是调用一个native方法去判断这个标识,从native方法的注释上可以解读到,它是通过参数ClearInterrupted
判断是否需要重置中断状态的,而传入true的的地方是public static boolean interrupted()
这个方法,我们看一下这个方法会在哪里被用到。

大部分都是在JUC包中用到,我们先随便挑个方法进去看一下,这里选择LinkedTransferQueue
类的transfer
方法
public void transfer(E e) throws InterruptedException {
if (xfer(e, true, SYNC, 0) != null) {
Thread.interrupted(); // failure possible only due to interrupt
throw new InterruptedException();
}
}
可以看到,它在调用Thread.interrupted();
后,对标记位进行重置,然后抛出了InterruptedException
异常,有没有觉得这里的设计和线程阻塞后,然后调用interrupt()
方法进行中断时抛出InterruptedException
异常是一样的,从这里也可以联想推测JVM底层阻塞方法的实现,应该也是先将线程的中断标记位重新设置为false,然后抛出异常。
因为笔者没有接触过C/C++,所以无法继续给大家剖析JVM底层的代码实现,只能通过在Java层面找到类似的设计进行类比,语言虽不同,但思想是相通的。
总结
今天主要讲了在Java中关闭一个线程的两种方式,
-
第一种是通过 stop()
方法强制中断,此方法不推荐使用,会出现不可控的问题; -
第二种是通过 interrupt()
方法,通过设置标记位进行中断,也是Java中推荐的方式,将中断选择权交给了线程,这里需要注意的是抛出InterruptedException
这个异常不意味着线程的结束,如果你使用到了线程的中断标识,记住它会对中断标识进行复位,当然你可以自己捕获到异常后进行自己想要的一些处理。
好了,今天的学习就到这里。读完记得 赞 一个,最新文章可以点击 阅读原文
原文始发于微信公众号(DevUnion):如何优雅地关闭一个线程
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/35459.html