手写守护线程优雅退出程序

一、使用背景

当一个程序中包含多个线程时,多个线程并发运行不同的业务,我们不能粗暴的直接退出程序,可以手动输入命令,程序读取到相关命令,代码中实现线程的退出,这样可以使当前运行线程的业务运行完毕在退出程序。

二、实现代码

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 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变量即会安全优雅退出

手写守护线程优雅退出程序_第1张图片

四、实现逻辑

      实现的大致逻辑就是,当启动停止程序时,在本例当中就是以特定的参数启动同一个程序,停止程序会调用父线程中的方法,写一个文件,写完之后,停滞程序立马退出。而守护线程会一直循环读取文件中的标志位,若标志位退出,则循环线程设置退出标识,使得各个线程运行完业务逻辑后再退出。最后退出程序。

五、相关知识点

在本次的案例中涉及到一下几点知识点

1.Thread.sleep(200)休眠规则

    在守护线程中,循环遍历线程集合时调用了Thread.sleep(200);方法,休眠的是当前线程,也就是本例中的守护线程FatherThread,我们可以大致认为,在哪个线程调用sleep方法,休眠的就是哪个线程。

2.start方法和run方法的区别

首先给大家看下在代码中调用run方法和调用start方法,控制台打印的情况

手写守护线程优雅退出程序_第2张图片

我们将thread0通过run方法运行,thread1通过start方法运行,我们可以看到,控制台只打印了thread0的打印信息,而thread1的并没有打印

手写守护线程优雅退出程序_第3张图片

而我们将thread1的启动改为run方法,则可以发现thread0和thread1中的信息都有打印。

那我们就要想这是为什么呢

     因为,Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法。

       而直接调用run方法则是运行线程类的一个普通方法而已,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。

      在我们的代码实例中,也就是main方法所在的主线程运行到thread0的run方法时,一直在运行该方法,而我们的线程在没有给退出标识是一直是死循环,所以就一直打印thread0的信息,根本就没有走到新建thread1并运行的步骤来。所以在实际生产中,多个线程运行时,用run方法启动线程根本就一个错误的用法,应该使用start方法

3.interrupt的作用

      interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,至于线程收到这个中断信号是否要结束线程,由收到线程决定。

    也许有人在网上搜索如何优雅的退出线程时,会搜索到一下代码

手写守护线程优雅退出程序_第4张图片

循环遍历线程,然后给每个线程设置一个状态,

手写守护线程优雅退出程序_第5张图片

然后在线程里判断当前线程是否中断,如果中断则退出线程。

当然业务逻辑简单时,这样写没有问题,那么就有人问了,明明可以很简单的写,为什么你前面守护线程中退出这么麻烦

    首先我们来但看子线程,在代码执行到判断线程是否中断时,直接退出了线程,这对于这时的业务逻辑运行肯定是没有问题的,然后我们再来看守护线程,给每个线程设置完中断状态,然后直接退出,这是假如有一个线程他的业务逻辑还没有处理完呢,这样直接退出就会有问题。也就是假如说我运行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);
        }

 

 

 

你可能感兴趣的:(多线程)