一、使用背景
当一个程序中包含多个线程时,多个线程并发运行不同的业务,且多个线程并未使用线程池,我们不能粗暴的直接退出程序,可以手动输入命令,程序读取到相关命令,代码中实现线程的退出,这样可以使当前运行线程的业务运行完毕在退出程序。
二、实现代码
main方法所在类
/**
* @ClassName: TestMain
* @Author: tanp
* @Description: ${description}
* @Date: 2020/6/11 20:43
*/
public class TestMain {
public static void main(String[] args) {
if (args != null && args.length > 0 && args[0].equals("exit")) {
FatherThread.writeDate("exit", true);
return;
}
FatherThread fatherThread = new FatherThread();
TestThread thread0 = new TestThread();
thread0.start();
fatherThread.addThread(thread0);
TestThreadOne thread1 = new TestThreadOne();
thread1.start();
fatherThread.addThread(thread1);
fatherThread.start();
}
}
守护线程
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* @Package: PACKAGE_NAME
* @ClassName: FatherThread
* @Author: tanp
* @Description: ${description}
* @Date: 2020/6/11 20:46
*/
public class FatherThread extends Thread {
public final static String path = "running";
List<FatherThread> list = new ArrayList<>();
boolean exit = false;
@Override
public void run() {
File file = new File(path);
if (file.exists()) {
if (!file.delete()) {
System.out.println("删除标识文件异常");
}
}
while (true) {
String flageValue = getFlagValue();
if ("true".equals(flageValue)) {
System.out.println("开始关闭进程");
for (int i = 0; i < list.size(); i++) {
list.get(i).interrupt();
while (list.get(i).getState() != Thread.State.TERMINATED) {
try {
Thread.sleep(200);
System.out.println("ccc"+ Thread.currentThread().getName());
list.get(i).interrupt();
list.get(i).stopThread();
} catch (InterruptedException e) {
System.out.println("进程停止异常");
}
}
System.out.println("线程:" + list.get(i).getName() + ",停止成功");
}
if (file.exists()) {
if (!file.delete()) {
System.out.println("删除标识文件异常");
}
}
System.out.println("应用已停止");
System.exit(0);
}
}
}
private void stopThread() {
this.exit = true;
}
private String getFlagValue() {
File file = new File(path);
FileInputStream in = null;
try {
if (!file.exists()) {
return "";
}
in = new FileInputStream(file);
Properties properties = new Properties();
properties.load(in);
return properties.getProperty("exit");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return "";
}
public static void writeDate(String exit, boolean b) {
File file = new File(path);
FileOutputStream out = null;
if (!file.exists()) {
try {
if (!file.createNewFile()) {
System.out.println("创建标志文件失败");
}
out = new FileOutputStream(file);
Properties properties = new Properties();
properties.setProperty("exit", "true");
properties.store(out, null);
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public void addThread(FatherThread thread) {
list.add(thread);
}
}
测试线程1
/**
* @ClassName: TestThread
* @Author: tanp
* @Description: ${description}
* @Date: 2020/6/11 20:48
*/
public class TestThread extends FatherThread{
@Override
public void run() {
while (!exit){
System.out.println("线程0正在运行");
try {
Thread.sleep(5000L);
System.out.println("00"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试线程2
/**
* @ClassName: TestThreadOne
* @Author: tanp
* @Description: ${description}
* @Date: 2020/6/11 20:50
*/
public class TestThreadOne extends FatherThread{
@Override
public void run() {
while (!exit){
System.out.println("线程1正在运行");
try {
Thread.sleep(5000L);
System.out.println("11"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
三、程序运行及代码展示
首先运行程序,当想要停止该程序时,再次运行程序但是需要给入参数exit,原先运行的程序读取到exit变量即会安全优雅退出
四、实现逻辑
实现的大致逻辑就是,当启动停止程序时,在本例当中就是以特定的参数启动同一个程序,第二次以特定参数启动的程序会调用父线程中的方法,写一个文件,写完之后,该程序立马return退出。而原先启动的程序中的守护线程会一直循环读取特定文件中的标志位,该标志位则是第二次启动程序写进来的,若读取到标志位退出,则原先的程序中的循环线程设置退出标识,使得各个线程运行完业务逻辑后再退出。最后退出程序。
五、相关知识点
在本次的案例中涉及到一下几点知识点
1.Thread.sleep(200)休眠规则
在守护线程中,循环遍历线程集合时调用了Thread.sleep(200);方法,休眠的是当前线程,也就是本例中的守护线程FatherThread,我们可以大致认为,在哪个线程调用sleep方法,休眠的就是哪个线程。
2.start方法和run方法的区别
首先给大家看下在代码中调用run方法和调用start方法,控制台打印的情况
我们将thread0通过run方法运行,thread1通过start方法运行,我们可以看到,控制台只打印了thread0的打印信息,而thread1的并没有打印
而我们将thread1的启动改为run方法,则可以发现thread0和thread1中的信息都有打印。
那我们就要想这是为什么呢
因为,Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法。
而直接调用run方法则是运行线程类的一个普通方法而已,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。
在我们的代码实例中,也就是main方法所在的主线程运行到thread0的run方法时,一直在运行该方法,而我们的线程在没有给退出标识是一直是死循环,所以就一直打印thread0的信息,根本就没有走到新建thread1并运行的步骤来。所以在实际生产中,多个线程运行时,用run方法启动线程根本就一个错误的用法,应该使用start方法
3.interrupt的作用
interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,至于线程收到这个中断信号是否要结束线程,由收到线程决定。
也许有人在网上搜索如何优雅的退出线程时,会搜索到一下代码
循环遍历线程,然后给每个线程设置一个状态,
然后在线程里判断当前线程是否中断,如果中断则退出线程。
当然业务逻辑简单时,这样写没有问题,那么就有人问了,明明可以很简单的写,为什么你前面守护线程中退出这么麻烦
首先我们来但看子线程,在代码执行到判断线程是否中断时,直接退出了线程,这对于这时的业务逻辑运行肯定是没有问题的,然后我们再来看守护线程,给每个线程设置完中断状态,然后直接退出,这是假如有一个线程他的业务逻辑还没有处理完呢,这样直接退出就会有问题。也就是假如说我运行thread0线程,在没有设置中断状态时,我运行了一段业务逻辑,并且业务逻辑还没有运行完,然后我设置了中断状态,这时又运行到了thread0线程判断含有中断状态,退出线程。可是这时我明明thread0上次运行时还有一些业务逻辑未处理,而我就退出了系统,这样明显是有问题的。
所以,在我们的守护线程中,我们是先给线程设置中断状态,设置完后,判断线程是否不是完工状态,不是完工状态,守护线程休眠一下,休眠完后,再给子线程设置中断状态,并设置退出线程标志位,而子线程收到退出的标志位,也不会再往下运行其他业务逻辑,这时就只要考虑子线程之前运行的业务逻辑,只有当前正在遍历的子线程的线程状态为完工时,才会跳出当前循环,对下一个线程执行同样的操作,当所有循环的线程都完工状态时,跳出最外面for循环,退出程序
4.线程池判断
其实我们上面的代码写的这么麻烦,我们如果运用了线程池,则可以很简单的实现所有线程都完工后才关闭程序,下面展示一段不完整的代码,以供大家思路散发
//线程退出标识,直接设置线程退出标识,单个线程读取到该标识,则直接退出线程
StaticValues.IS_EXIT = true;
spGateThreadExecutor.shutdown();
log.info("程序正在退出......");
while (spGateThreadExecutor.getActiveCount() != 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("线程休眠异常", e);
}
}
log.info("程序正常退出");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("线程休眠异常", e);
}