1.概述
日常业务中,可能会遇到这样一个场景:终止一个正在运行的线程。停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作。优雅地停止线程是java开发中比较重要的技术点,因此需要一些技巧。本文将基于多线程停止的几种方式来进行分析,并对比各种方式的优劣。
2.线程停止的几种方法
首先要了解JAVA中终止正在运行线程的三种方法:
2.1 调用Thread.stop()方法来强行终止线程
2.1.1 一个小案例
这种方式是不被推荐的,因为stop()、suspend()与resume()都是作废过期的方法,使用这些方法会带来不可预料的后果。尤其是stop()方法,会强行终止线程,可能造成数据不一致的后果,最终导致程序执行流程异常。
首先看一个线程终止的案例:
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadStopRunnable extends Thread {
@Override
public void run() {
while (true) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
Date currentDate = new Date();
String date = simpleDateFormat.format(currentDate);
log.error("当前线程:{},正在运行,时间为:{}", Thread.currentThread().getName(), date);
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
log.error("线程休眠异常:{}", e);
}
}
}
}
线程执行代码如下:
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadStop {
public static void main(String[] args) {
ThreadStopRunnable threadStopRunnable = new ThreadStopRunnable();
Thread thread = new Thread(threadStopRunnable);
thread.start();
try {
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
log.error("主线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
}
thread.stop();
log.info("主线程:{}结束", Thread.currentThread().getName());
}
}
程序运行结果如下:
上述第一段代码中设置了一个死循环,每隔2s打印一下当前线程的运行状态和时间;第二段代码为线程执行代码,这段代码休眠10s后调用了stop()方法终止了第一段代码的执行。由上述代码可知,stop()方法确实能够终止线程的运行。但是stop()方法是一个非常暴力的方法,需要谨慎使用。
2.1.2 stop()方法与java.lang.ThreadDeath异常
调用stop()方法会抛出java.lang.ThreadDeath异常,通常情况下不需要显示捕捉该异常,该异常是 Error 的子类而不是 Exception 的子类,单纯通过捕获所有Exception异常是无法捕捉该异常的。具体该异常捕获情况如下:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ThreadStopRunable extends Thread {
@Override
public void run() {
try {
this.stop();
} catch (Error e) {
log.error("线程已终止:{}", e.toString());
}
}
}
线程执行代码如下:
@Slf4j
public class ThreadStop {
public static void main(String[] args) {
ThreadStopRunable threadStop1 = new ThreadStopRunable();
threadStop1.start();
}
}
执行结果如下所示:
2.2 使用标记位来退出线程
这种方式主要实现方式是:定义一个标记位,当线程run()方法运行完成之后,修改标记位状态为false,进而达到终止线程的目标。修改上述代码如下:
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadStopRunnable extends Thread {
public boolean flag = true;
@Override
public void run() {
while (flag) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
Date currentDate = new Date();
String date = simpleDateFormat.format(currentDate);
log.error("当前线程:{},正在运行,时间为:{}", Thread.currentThread().getName(), date);
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
log.error("线程休眠异常:{}", e);
}
}
}
public void setFlag() {
this.flag = false;
log.info("setFlag:{}", false);
}
}
线程执行代码如下:
@Slf4j
public class ThreadStop {
public static void main(String[] args) {
ThreadStopRunnable thread = new ThreadStopRunnable();
thread.start();
try {
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
log.error("主线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
}
//修改线程状态为false,终止线程执行
thread.setFlag();
log.info("主线程:{}结束", Thread.currentThread().getName());
}
}
执行结果如下:
主线程执行后,运行执行线程,主线程休眠10s后,调用执行线程终止方法,子线程运行结束。
2.3 使用interrupt()方法终止线程
2.3.1 无法停止线程
interrupt()方法可以停止线程,但interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。观察并运行以下代码:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadStopRunnable extends Thread {
public boolean flag = true;
@Override
public void run() {
while (flag) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
Date currentDate = new Date();
String date = simpleDateFormat.format(currentDate);
log.info("当前线程:{},正在运行,时间为:{}", Thread.currentThread().getName(), date);
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
log.error("线程休眠异常:{}", e);
}
}
}
}
主线程如下:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadStop {
public static void main(String[] args) {
ThreadStopRunnable thread = new ThreadStopRunnable();
thread.start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
log.error("主线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
}
//调用interrupt方法
thread.interrupt();
log.info("主线程:{}结束", Thread.currentThread().getName());
}
}
程序运行结果如下:
由上述结果可知,当调用interrupt()方法之后,程序并没有立刻停止,主线程main结束后,子线程依旧保持运行。interrupt()的作用是中断此线程,注意此线程不一定是当前线程,而是值调用该方法的Thread实例所代表的线程,调用interrupt方法不会真正的结束线程,在当前线程中打上一个停止的标记,线程仍然会继续运行。
由上图可知,结果中抛出了一个sleep interrupted异常,这里涉及一个知识点:如果在sleep状态下停止某一线程(调用interrupt方法),线程会抛出异常,并且清除停止状态值,使之变成false。
2.3.2 判断线程停止状态
Thread类提供了interrupted()方法来测试当前线程是否被中断(检查该线程的中断标志),调用该方法会返回一个boolean值并清除中断的状态,第二次调用时中断状态已经被清除,所以会返回false。下面看一段代码:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadInterrupted {
public static void main(String[] args) {
Interrupted thread = new Interrupted();
thread.setName("Interrupted-thread");
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
}
thread.interrupt();
//thread.isInterrupted方法是检查Interrupted是否被打上停止的标记
log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
//Thread.interrupted方法是检查主线程是否打上停止的标记
log.info("线程:{},是否中断:{}", Thread.currentThread().getName(), Thread.interrupted());
}
}
@Slf4j
class Interrupted extends Thread {
@Override
public void run() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
while (true) {
Date date = new Date();
String format = simpleDateFormat.format(date);
log.info("当前时间:{}", format);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
}
}
}
}
上述程序的运行结果如下:
interrupted()与isInterrupted()的唯一区别在于:前者会读取并清除中断状态,后者仅读取状态。
为了验证上述这一结论,首先看一段Thread的源码:
/**
* 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);
}
这是interrupted()在Thread类中的源码,它的上面有一段注释,翻译后该意思如下:
测试当前线程是否已经中断。线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。
为了验证上述这段话,我们写一段代码来测试一下:
@Slf4j
public class ThreadInterrupted {
public static void main(String[] args) {
log.info("当前线程:{},启动成功!");
Thread.currentThread().interrupt();
log.info("当前线程:{},是否终止:{}",Thread.currentThread().getName(),Thread.interrupted());
log.info("当前线程:{},是否终止:{}",Thread.currentThread().getName(),Thread.interrupted());
}
}
上述代码的运行结果为:
上述结果印证了interrupted()方法具有清除状态的功能,所以第二次打印显示的值为false。
同理,我们也需要验证一下isinterrupted()方法是否会清除状态?验证代码如下:
@Slf4j
public class ThreadInterrupted {
public static void main(String[] args) {
Interrupted thread = new Interrupted();
thread.setName("Interrupted-thread");
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
}
thread.interrupt();
log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
}
}
@Slf4j
class Interrupted extends Thread {
@Override
public void run() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
while (true) {
Date date = new Date();
String format = simpleDateFormat.format(date);
log.info("当前时间:{}", format);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
}
}
}
}
运行结果如下所示:
从结果中可以看到,方法 isInterrupted()并未清除状态标志,所以打印了两个true。
由上述一系列代码可以得出以下结论:
this.interrupted() :测试当前线程是否已经是中断状态,执行后具有将状态标志置清除为false的功能。
this.isInterrupted() :测试线程Thread对象是否已经是中断状态,但不清除状态标志。
2.3.3 如何正确停止线程
上面讲述了关于如何判断线程是否处于中断状态,并且interrupte()方法不能直接终止线程,那么要如何安全终止线程呢?这里可以使用抛出异常法来终止线程。具体可以参考下面案例:
@Slf4j
public class ThreadInterrupted {
public static void main(String[] args) {
Interrupted thread = new Interrupted();
thread.setName("Interrupted-thread");
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
}
thread.interrupt();
log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
}
}
@Slf4j
class Interrupted extends Thread {
@SneakyThrows
@Override
public void run() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
while (true) {
if (this.isInterrupted()) {
log.error("线程:{},已经被终止",Thread.currentThread().getName());
throw new InterruptedException("线程被终止");
//return;
}
Date date = new Date();
String format = simpleDateFormat.format(date);
log.info("当前时间:{}", format);
}
}
}
上述线程运行结果如下:
这种方式主要在子线程中加了一个状态检测判断,如果判断该线程的中断状态为true,则直接抛出异常,进而达到终止线程的目的。这种方式一旦检测到了中断状态,就不会再向下执行该线程的代码,保证了安全性。事实上,如果不想抛出异常,可以在检测线程中断状态为true后,直接利用return终止线程,也就是将上述代码throw new InterruptedException("线程被终止");
换成return;
。
3.注意事项
3.1 使用stop()方法造成读写不一致案例
为了验证stop()方法的不安全型,编写以下案例加以验证。
首先编写一个User类,包含两个属性:id和userName。
import lombok.Data;
@Data
public class User {
public User() {
this.id = 0;
this.userName="0";
}
private Integer id;
private String userName;
}
其次编写一个写线程:
import java.util.concurrent.TimeUnit;
@Slf4j
public class WriteThread extends Thread {
private User user;
public WriteThread(User user) {
this.user = user;
}
@Override
public void run() {
while (true) {
synchronized (user) {
int currentTime = (int) (System.currentTimeMillis() / 1000);
user.setId(currentTime);
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
log.error("线程休眠异常:{}", e);
}
user.setUserName(String.valueOf(currentTime));
}
Thread.yield();
}
}
}
再编写一个读线程:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ReadThread extends Thread {
private User user;
public ReadThread(User user) {
this.user = user;
}
@Override
public void run() {
while (true) {
synchronized (user) {
while (!user.getUserName().equals(user.getId().toString())) {
log.info("userId:{},userName:{}", user.getId(), user.getUserName());
}
}
Thread.yield();
}
}
}
最后编写主线程代码:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class RunnableThread {
public static void main(String[] args) {
User user = new User();
ReadThread readThread = new ReadThread(user);
readThread.start();
while (true) {
WriteThread writeThread = new WriteThread(user);
writeThread.start();
try {
TimeUnit.MICROSECONDS.sleep(200);
} catch (InterruptedException e) {
log.error("线程休眠异常:{}", e);
}
writeThread.stop();
}
}
}
可得到如下结果:
4.小结
1.stop()方法是一个废弃的方法,会造成数据不一致问题,建议不要使用;
2.interrupted()与isInterrupted()的唯一区别在于:前者会读取并清除中断状态,后者仅读取状态;
3.利用isInterrupted()方法判断线程运行状态,再结合抛出异常法,可以有效并安全终止线程。
4.参考文献
1.《JAVA多线程编程核心技术》-高洪岩著
2.https://juejin.cn/post/7019594104229068831