如何优雅的停止一个线程?

在之前的文章中 i-code.online -《并发编程-线程基础》我们介绍了线程的创建和终止,从源码的角度去理解了其中的细节,那么现在如果面试有人问你 “如何优雅的停止一个线程?”, 你该如何去回答尼 ?能不能完美的回答尼?

  • 对于线程的停止,通常情况下我们是不会去手动去停止的,而是等待线程自然运行至结束停止,但是在我们实际开发中,会有很多情况中我们是需要提前去手动来停止线程,比如程序中出现异常错误,比如使用者关闭程序等情况中。在这些场景下如果不能很好地停止线程那么就会导致各种问题,所以正确的停止程序是非常的重要的。

强行停止线程会怎样?


  • 在我们平时的开发中我们很多时候都不会注意线程是否是健壮的,是否能优雅的停止,很多情况下都是贸然的强制停止正在运行的线程,这样可能会造成一些安全问题,为了避免造成这种损失,我们应该给与线程适当的时间来处理完当前线程的收尾工作, 而不至于影响我们的业务。

  • 对于 Java 而言,最正确的停止线程的方式是使用 interrupt。但 interrupt 仅仅起到通知被停止线程的作用。而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。可能很多同学会疑惑,既然这样那这个存在的意义有什么尼,其实对于 Java 而言,期望程序之间是能够相互通知、协作的管理线程

  • 比如我们有线程在进行 io 操作时,当程序正在进行写文件奥做,这时候接收到终止线程的信号,那么它不会立马停止,它会根据自身业务来判断该如何处理,是将整个文件写入成功后在停止还是不停止等都取决于被通知线程的处理。如果这里立马终止线程就可能造成数据的不完整性,这是我们业务所不希望的结果。

interrupt 停止线程

  • 关于 interrupt 的使用我们不在这里过多阐述,可以看 i-code.online -《并发编程-线程基础》文中的介绍,其核心就是通过调用线程的 isInterrupt() 方法进而判断中断信号,当线程检测到为 true 时则说明接收到终止信号,此时我们需要做相应的处理

  • 我们编写一个简单例子来看
 Thread thread = new Thread(() -> {
            while (true) {
                //判断当前线程是否中断,
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程1 接收到中断信息,中断线程...中断标记:" + Thread.currentThread().isInterrupted());
                	//跳出循环,结束线程
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "线程正在执行...");

            }
        }, "interrupt-1");
        //启动线程 1
        thread.start();

        //创建 interrupt-2 线程
        new Thread(() -> {
            int i = 0;
            while (i <20){
                System.out.println(Thread.currentThread().getName()+"线程正在执行...");
                if (i == 8){
                    System.out.println("设置线程中断...." );
                    //通知线程1 设置中断通知
                    thread.interrupt();

                }
                i ++;
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"interrupt-2").start();

上述代码相对比较简单,我们创建了两个线程,第一个线程我们其中做了中断信号检测,当接收到中断请求则结束循环,自然的终止线程,在线程二中,我们模拟当执行到 i==8 时通知线程一终止,这种情况下我们可以看到程序自然的进行的终止。

这里有个思考: 当处于 sleep 时,线程能否感受到中断信号?

  • 对于这一特殊情况,我们可以将上述代码稍微修改即可进行验证,我们将线程1的代码中加入 sleep 同时让睡眠时间加长,让正好线程2通知时线程1还处于睡眠状态,此时观察是否能感受到中断信号
        //创建 interrupt-1 线程

        Thread thread = new Thread(() -> {
            while (true) {
                //判断当前线程是否中断,
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程1 接收到中断信息,中断线程...中断标记:" + Thread.currentThread().isInterrupted());
                    Thread.interrupted(); // //对线程进行复位,由 true 变成 false
                    System.out.println("经过 Thread.interrupted() 复位后,中断标记:" + Thread.currentThread().isInterrupted());

                    //再次判断是否中断,如果是则退出线程
                    if (Thread.currentThread().isInterrupted()) {
                        break;
                    }
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "线程正在执行...");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "interrupt-1");

我们执行修改后的代码,发现如果 sleepwait 等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。这样一来就不用担心长时间休眠中线程感受不到中断了,因为即便线程还在休眠,仍然能够响应中断通知,并抛出异常。

对于线程的停止,最优雅的方式就是通过 interrupt 的方式来实现,关于他的详细文章看之前文章即可,如 InterruptedException 时,再次中断设置,让程序能后续继续进行终止操作。不过对于 interrupt 实现线程的终止在实际开发中发现使用的并不是很多,很多都可能喜欢另一种方式,通过标记位。

用 volatile 标记位的停止方法

  • 关于 volatile 作为标记位的核心就是他的可见性特性,我们通过一个简单代码来看:

/**
 * @ulr: i-code.online
 * @author: zhoucx
 * @time: 2020/9/25 14:45
 */
public class MarkThreadTest {

    //定义标记为 使用 volatile 修饰
    private static volatile  boolean mark = false;

    @Test
    public void markTest(){
        new Thread(() -> {
            //判断标记位来确定是否继续进行
            while (!mark){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程执行内容中...");
            }
        }).start();

        System.out.println("这是主线程走起...");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //10秒后将标记为设置 true 对线程可见。用volatile 修饰
        mark = true;
        System.out.println("标记位修改为:"+mark);
    }
}

上面代码也是我们之前文中的,这里不再阐述,就是一个设置标记,让线程可见进而终止程序,这里我们需要讨论的是,使用 volatile 是真的都是没问题的,上述场景是没问题,但是在一些特殊场景使用 volatile 时是存在问题的,这也是需要注意的!

volatile 修饰标记位不适用的场景

  • 这里我们使用一个生产/消费的模式来实现一个 Demo

/**
 * @url: i-code.online
 * @author: zhoucx
 * @time: 2020/10/12 10:46
 */
public class Producter implements Runnable {

    //标记是否需要产生数字
    public static volatile boolean mark = true;

    BlockingQueue<Integer> numQueue;

    public Producter(BlockingQueue numQueue){
        this.numQueue = numQueue;
    }

    @Override
    public void run() {
        int num = 0;
        try {
            while (num < 100000 && mark){
                //生产数字,加入到队列中
                if (num % 50 == 0 ){
                    System.out.println(num + " 是50的倍数,加入队列");
                    numQueue.put(num);
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("生产者运行结束....");
        }
    }
}

首先,声明了一个生产者 Producer,通过 volatile 标记的初始值为 true 的布尔值 mark 来停止线程。而在 run() 方法中,while 的判断语句是 num 是否小于 100000 及 mark 是否被标记。while 循环体中判断 num 如果是 50 的倍数就放到 numQueue 仓库中,numQueue 是生产者与消费者之间进行通信的存储器,当 num 大于 100000 或被通知停止时,会跳出 while 循环并执行 finally 语句块,告诉大家“生产者运行结束”


/**
 * @url: i-code.online
 * @author: zhoucx
 * @time: 2020/10/12 11:03
 */
public class Consumer implements Runnable{

    BlockingQueue numQueue;

    public Consumer(BlockingQueue numQueue){
        this.numQueue = numQueue;
    }

    @Override
    public void run() {

        try {
            while (Math.random() < 0.97){
                //进行消费
                System.out.println(numQueue.take()+"被消费了...");;
                TimeUnit.MILLISECONDS.sleep(100);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("消费者执行结束...");
            Producter.mark = false;
            System.out.println("Producter.mark = "+Producter.mark);
        }

    }
}


而对于消费者 Consumer,它与生产者共用同一个仓库 numQueue,在 run() 方法中我们通过判断随机数大小来确定是否要继续消费,刚才生产者生产了一些 50 的倍数供消费者使用,消费者是否继续使用数字的判断条件是产生一个随机数并与 0.97 进行比较,大于 0.97 就不再继续使用数字。


/**
 * @url: i-code.online
 * @author: zhoucx
 * @time: 2020/10/12 11:08
 */
public class Mian {


    public static void main(String[] args) {
        BlockingQueue queue = new LinkedBlockingQueue(10);

        Producter producter = new Producter(queue);
        Consumer consumer = new Consumer(queue);

        Thread thread = new Thread(producter,"producter-Thread");
        thread.start();
        new Thread(consumer,"COnsumer-Thread").start();

    }
}

主函数中很简单,创建一个 公共仓库 queue 长度为10,然后传递给两个线程,然后启动两个线程,当我们启动后要注意,我们的消费时有睡眠 100 毫秒,那么这个公共仓库必然会被生产者装满进入阻塞,等待消费。


当消费者不再需要数据,就会将 canceled 的标记位设置为 true,理论上此时生产者会跳出 while 循环,并打印输出“生产者运行结束”。


然而结果却不是我们想象的那样,尽管已经把 Producter.mark设置成 false,但生产者仍然没有停止,这是因为在这种情况下,生产者在执行 numQueue.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断 Producter.mark的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理。

总结



通过上面的介绍我们知道了,线程终止的主要两种方式,一种是 `interrupt` 一种是`volatile` ,两种类似的地方都是通过标记来实现的,不过`interrupt` 是中断信号传递,基于系统层次的,不受阻塞影响,而对于 `volatile` ,我们是利用其可见性而顶一个标记位标量,但是当出现阻塞等时无法进行及时的通知。

在我们平时的开发中,我们视情况而定,并不是说必须使用 `interrupt` ,在一般情况下都是可以使用 `volatile` 的,但是这需要我们精确的掌握其中的场景。

本文由AnonyStar 发布,可转载但需声明原文出处。
仰慕「优雅编码的艺术」 坚信熟能生巧,努力改变人生
欢迎关注微信公账号 :云栖简码 获取更多优质文章
更多文章关注笔者博客 :云栖简码

原文地址:https://www.cnblogs.com/i-code/p/13804624.html