乐趣区

关于java-ee:如何优雅的停止一个线程

在之前的文章中 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 公布, 可转载但需申明原文出处。
企慕「优雅编码的艺术」深信游刃有余,致力扭转人生
欢送关注微信公账号:云栖简码 获取更多优质文章
更多文章关注笔者博客:云栖简码

退出移动版