如何暂停一个正在运行的线程?

今天把小伙伴问懵了,小刚,你知道怎么停止一个线程吗?

这...,这...,stop?

原来平时小刚这小子只知道创建线程,不知道怎么暂停线程呀~[狗头]


停止线程是在多线程开发中很重要的技术点,比如在多线程持续处理业务代码时,由于处理逻辑中有第三方接口异常,我们就假设发送短信接口挂了吧,那么此时多线程调用短信接口是没有任何意义的,我们希望接口恢复后再对接口进行处理,那么此时怎么办呢,如何中止已经启动的线程呢?

其实在Java中有3种方式可以终止正在运行的线程:

  1. 使用stop方法强制退出:使用stop()方法强制终止线程,注意,强烈不推荐这种方式,并且该方法已经被标记为过期方法了。
  2. 使用interrupt方法中断线程,该方法只是告诉线程要终止,但最终何时终止取决于计算机;
  3. 设置标志位:使用设置退出标志,使线程正常退出,也就是当run方法完成后线程终止;

尽管罗列了三种方式,但由于存在安全问题,所以舍弃了stop()方法,怎么就安全问题了呢?

暴力停止线程的stop()方法「禁止使用」

之所以说stop()方法暴力是相对于其他两种方式的,只要调用stop()方法,运行中的线程就暂停了,我们通过一段代码测试一下:

public class MyTest {

    public static void main(String[] args) {

        try {
            /**创建线程**/
            ThreadDemo demo = new ThreadDemo();
            /**开启线程**/
            demo.start();
            /**线程休眠**/
            Thread.sleep(5000);
            /**停止线程**/
            demo.stop();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

public class ThreadDemo extends Thread{

    /**变量i**/
    private int i = 0;

    @Override
    public void run() {
        try {
            while (true){
                i++;
                System.out.println("输出i:"+i);
                Thread.sleep(1000);
            }
        }catch (InterruptedException e){
            System.out.println("抛出异常");
        }
    }
}

执行结果如下:

输出i:1
输出i:2
输出i:3
输出i:4
输出i:5

如上,我们创建了一个死循环输出的线程ThreadDemo,每隔一秒输出i++,但是当遇到stop()方法后,就不再输出了,不对,看上去没问题呀,stop() 方法这不用的好好的吗?

嗨,怪就怪这个例子太简单了吧,我们来看看弄点带操作对象的例子,首先创建一个用户实体:

public class UserModel {

    /**
     * 给定userName+password默认值
     * 用于模拟上一个线程给赋的旧值
     */
    private String userName = "张三";

    private String password = "hahahha";

    /**
     * 用于复制的方法
     * 为防止多线程数据错乱,加上synchronized关键字
     * @param userName
     * @param password
     */
    synchronized public void setValue(String userName, String password){
        try {
            this.userName = userName;
            Thread.sleep(3000);
            this.password = password;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    省略get\set方法...
}

然后我们再在ThreadDemo中使用这个实体:

public class ThreadDemo extends Thread{

    private UserModel userModel;

    public ThreadDemo(UserModel userModel){
        this.userModel = userModel;
    }

    @Override
    public void run() {
        /**
         * 重新设置用户名+密码
         * 用户名:niceyoo
         * 密码:123456
         */
        userModel.setValue("niceyoo","123456");
    }
}

然后在MyTest中创建并启动线程,然后电泳stop()方法:

public class MyTest {

    public static void main(String[] args) {

        try {
            /**创建用户实体**/
            UserModel userModel = new UserModel();
            /**创建线程**/
            ThreadDemo demo = new ThreadDemo(userModel);
            /**开启线程**/
            demo.start();
            /**线程休眠**/
            Thread.sleep(1000);
            /**停止线程**/
            demo.stop();
            /**输出用户实体**/
            System.out.println(userModel.getUserName() + " :" + userModel.getPassword());
        } catch (ThreadDeath | InterruptedException e) {
            e.printStackTrace();
        }

    }

}

输出结果如下:

niceyoo :hahahha

显然跟我们预期的输出结果niceyoo\123456不一致,使用stop()释放锁,对锁定的对象进行了解锁,导致数据得不到同步的处理,出现数据不一致的情况,所以这样就会导致数据安全问题,这也是现在为何 stop() 方法被标注为 "作废、过期"。

interrupted()方法「只告诉要停止,不知道何时停」

使用interrupted()方法就不像是stop()方法那样简单粗暴了,调用该方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程,就好比,我打电话告诉你不要玩游戏了,但是你什么时候停止玩游戏就是你的事了。

public class MyTest {

    public static void main(String[] args) {
        try {
            /**创建线程**/
            ThreadDemo2 demo = new ThreadDemo2();
            /**开启线程**/
            demo.start();
            /**线程休眠**/
            Thread.sleep(2000);
            /**停止线程**/
            demo.interrupt();
        } catch (InterruptedException e) {
            System.out.println("线程已经暂停");
            e.printStackTrace();
        }
    }

}

public class ThreadDemo2 extends Thread{

    @Override
    public void run() {
        try {
            for (int i = 0; i < 1800000; i++) {

                if(!this.isInterrupted()){
                    System.out.println("输出i:"+i++ + " - 线程未停止 ");
                }else{
                    System.out.println("输出i:"+i++ + " - 线程已停止 - 抛出异常");
                    throw new InterruptedException();
                }
            }
        }catch (InterruptedException e){
            System.out.println("线程已结束...");
        }
    }
}

输出结果:

输出i:1499992 - 线程未停止 
...
输出i:1700624 - 线程未停止 
输出i:1700626 - 线程未停止 
输出i:1700628 - 线程已停止 - 抛出异常
线程已结束...

简单说一下上方代码,首先我们创建了一个for循环输出i++的线程,启动线程后调用 interrupt() 方法停止线程,但是啥时候停止是不可控的,虽然不可控但是还是有方法知道线程是否是停止的,我们在ThreadDemo2线程类中看到 if 判断 — this.isInterrupted() 「等价于Thread.currentThread().isInterrupt() 」,这是用来判断当前线程是否被终止,通过这个判断我们可以做一些业务逻辑处理,通常如果this.isInterrupted被判定为true后,我们会抛一个中断异常,然后通过try-catch捕获。

再额外说一下,有的小伙伴设置的 for 循环变量的最大值比较小,测试执行过程中并没有重现线程被终止,然后就怀疑这个 interrupt() 到底能不能停止线程呀, 不用纠结,这正是线程的自主权,我们无法像 stop() 方法一样直接停止线程的。

设置标志位

设置标志位是用到了共享变量的方式,我们了解线程对于变量的操作都是操作的变量副本,而一旦使用

volatile关键字修饰后,因为其可见性,变量变更始将终从主存中获取最新值。

public class MyTest {

    public static void main(String[] args) {
        /**创建2个线程**/
        ThreadDemo3 demo1 = new ThreadDemo3();
        ThreadDemo3 demo2 = new ThreadDemo3();
        demo1.setName("线程1");
        demo2.setName("线程2");
        /**开启线程**/
        demo1.start();
        demo2.start();
        /**让线程先运行5s**/
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        /**修改线程的变量**/
        demo1.heartbeat = false;
        demo2.heartbeat = false;

        System.out.println("----暂停线程----");

        /**让线程再运行5s**/
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        /**再将标志为置为true**/
        demo1.heartbeat = true;
        demo2.heartbeat = true;

        System.out.println("----从新开启线程----");
    }
}

public class ThreadDemo3 extends Thread{
    /**共享变量**/
    volatile Boolean heartbeat = true;

    @Override
    public void run() {
        while (true){
            /**判断标志是否为true**/
            if (heartbeat){
                System.out.println("当前运行线程为:" +Thread.currentThread().getName() + " - 运行");
            }else{
                System.out.println("当前运行线程为:" +Thread.currentThread().getName() + " - 非运行");
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:

省略ing...
当前运行线程为:线程1 - 运行
当前运行线程为:线程2 - 运行
----暂停线程----
省略ing...
当前运行线程为:线程1 - 非运行
当前运行线程为:线程2 - 非运行
----从新开启线程----
当前运行线程为:线程1 - 运行
当前运行线程为:线程2 - 运行
省略ing...

来看一下上方代码,我们在线程类里创建了共享变量heartbeat,因为要监听这个共享变量的状态,肯定是要用while循环体了,为了演示状态的变更,所以在while循环体代码中没有throw抛出 InterruptedException 异常,正常情况下在判断共享变量为fasle时,也是要手动抛出异常的,ok,这就是设置标志位了。

总结一定要看的

stop()方法在这就不提了,肯定是行不通的,至于为何不能使用大家可以再仔细看看上方那个例子。

然后是interrupt()方法+抛异常处理,看完上边那个例子,大家可能会觉得这个方法有点问题,暂停线程完全靠线程自身决定,即便调用了也不能快速的停止线程,但是我要告诉你,这是目前最为正确的方式... 咳咳,别着急,咱先把设置标志位说了。

设置标志位使用了volatile关键字共享变量方式,通过改变共享变量+抛异常的方式来暂停线程,这个看起来最有效,最正确的方式,其实有一点点问题,而这一点点问题就是为什么让 interrupt() 成为最正确的方式。

volatile标记共享变量方式,在线程发生阻塞时是无法完成响应的。

这个所谓的阻塞指的是什么呢?

其实发生阻塞的情况是比较常见的,比如调用 Thread.join() 方法「当前线程陷入无限期的阻塞,join() 所属的线程对象正常运行run()方法,对join()方法不了解的小伙伴可以去百度了」,或者是 Thread.sleep() 方法,再或者是线程需要等待键盘输入而被阻塞,还有socket网络编程中的 ServerSocket.accept() 方法等等等,总之,在这些种种情况下让线程处于不可运行状态时,即便是主线程修改了共享变量的值,该线程此时根本无法检查循环标志,所以也就无法实现线程中断。

所以,interrupt() + 手动抛异常的方式是目前中断一个正在运行的线程最为正确的方式了。

如果觉得这篇文章对你有用,可以右上角关注一下我呀~

关注我的公众号吧,与一万+小伙伴一起成长~

posted @ 2020-07-27 00:00  niceyoo  阅读(2291)  评论(0编辑  收藏  举报