当前位置:   article > 正文

深度解析线程的正确停止方法_线程停止

线程停止

一、解惑

1. 什么情况下,线程需要被停止?

线程和任务被创建和启动之后,大部分情况下都是自然运行到结束的,自然停止,但有些情况会需要用到停止线程,如:

  • 用户主动取消

  • 服务被快速关闭

  • 运行出错或超时情况下等线程都需要被停止

这些情况都需要主动来停止线程,想让线程安全可靠停止下来并不容易,Java语言没有一种机制来安全正确地停止线程,但是它提供了interrupt,这是一种协作机制。

2. 如何正确停止线程?

可以使用interrupt来通知中断,但它不是强制停止。

通俗理解:就是用一个线程来通知另一个线程让它停止工作,在Java中,如果想停止一个线程,能做的最多能做的就是告诉一个线程,你该中断了,而被中断的线程本身拥有决定权,它不仅能决定何时去响应这个中断,何时去停止,还拥有最高决定权,就是停不停止,也就是说,如果被停止线程不想被中断,那我们对此无能为力,根本没有能力做到强行停止。但是在开发过程中,开发的各个部门和小组都遵守良好的规范的话,是可以都把代码处理成可以响应interrupt中断来停止的,但这仅仅是一个规范,不是一种强制。

3. 我们是程序的控制者,凭什么我们没有控制线程停止的权利?

其实大多数时候我们想停止一个线程,都会至少让它运行到结束,比如说,即便我们关机的时候,也会在关机的时候做很多的收尾工作,结束一些进程、线程,保存一些状态,那么线程也是一样的,由于我们想中断的线程,可能不是我们开发的,对这个线程执行的业务逻辑根本就不熟悉,如果想让它停止,其实是希望它完成一系列的保存工作,交接工作,再停止,而不是立刻停止,让它陷入一种混乱的状态。所以,被停止的那个线程,对自己的业务逻辑是最熟悉的,而发出停止信号的线程,它对别人的业务逻辑很可能是不了解的,所以java语言设计的时候就把线程停止的权力和步骤交给了被停止线程本身,这就是停止线程的核心,而不是强制停止。

二. 停止线程的实践

1. 普通情况的线程中断

(1)直接使用 interrupt 方法,线程未停止
/**
 * 描述:run方法没有 sleep 和 wait方法
 * */
public class RightWayStopThreadWithOutSleep implements Runnable{
    @Override
    public void run() {
        int num = 0;
        while (num <= Integer.MAX_VALUE/2){
            if (num%10000 == 0){
                System.out.println(num+"是能被10000整除的数");
            }
            num++;
        }
        System.out.println("任务运行结束了");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithOutSleep());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

打印结果:

你会发现,没有任何效果,我们使用 interrupt 想把这个线程中断,但它似乎根本就没理会我们,这个线程想不想停止,取决于它本身,所以我们需要对它进行改变,去响应 interrupt 的中断,这样便能停止线程。

(2)增加中断响应 !Thread.currentThread().isInterrupted()

当线程被中断时,即Thread.currentThread().isInterrupted()等于 true时,!Thread.currentThread().isInterrupted()等于false,线程被停止。

/**
 * 描述:run方法没有 sleep 和 wait方法
 * */
public class RightWayStopThreadWithOutSleep implements Runnable{
    @Override
    public void run() {
        int num = 0;
        while (!Thread.currentThread().isInterrupted() && num < Integer.MAX_VALUE/2){
            if (num%10000 == 0){
                System.out.println(num+"是能被10000整除的数");
            }
            num++;
        }
        System.out.println("任务运行结束了");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithOutSleep());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

打印结果:

看打印结果中最后的数值与(1)中的打印结果相比,明显线程未执行完就被停止了,interrupt()方法中断有效。

2. 阻塞情况下的线程中断

代码演示:

/**
 * 描述:带sleep的中断线程方法
 * */
public class RightWayStopThreadWithSleep {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            try {
                while (!Thread.currentThread().isInterrupted() && num < 300){
                    if (num % 100 == 0){
                        System.out.println(num+"是能被100整除的数");
                    }
                    num++;
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        thread.interrupt(); // 睡眠 500 毫秒后执行 interrupt,线程应在执行到了 Thread.sleep(1000);这个阶段
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

打印结果:

发现报异常,回顾一下,当我们在写sleep()方法时,代码要求我们try-catch这个异常,然后打印的结果也是catch到了,即java.lang.InterruptedException: sleep interrupted,为什么会报异常呢,是这样的,当线程在休眠状态下,如果收到这个中断信号,线程便会响应这个中断,而响应这个中断的方式非常特殊,就是抛出这个异常,于是我们就在 catch 中打印了这个异常sleep interrupted,就是在sleep过程中被打断了。

所以当我们程序带有sleep,或者能让线程阻塞的方法,并且有可能被中断的时候,需要注意处理 InterruptedException 这个异常,我们可以放在 catch中处理,这样在线程进入阻塞过程中,依然可以响应这个中断并进行处理。

3. 线程每次迭代都阻塞

代码演示:

/**
 * 描述:带sleep的中断线程方法
 * */
public class RightWayStopThreadWithSleepEveryLoop {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            try {
                while (num <= 10000){
                    if (num % 100 == 0){
                        System.out.println(num+"是能被100整除的数");
                    }
                    num++;
                    Thread.sleep(10); // slepp方法放在循环里
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

打印结果:

当线程的阻塞方法是在循环中时,就不再需要 Thread.currentThread().isInterrupted() 判断线程是否被中断了,这是因为在整个循环过程中,代码的大部分运行时间都是消耗在了Thread.sleep(10) 中的,所以极有可能是在sleep时接收到interrupt,自然会抛出InterruptedException 异常,不需要在代码中加入 Thread.currentThread().isInterrupted()判断检查是否已中断。

4. 基于第3步的代码:如果While里面放 try / catch,会导致中断失效

/**
 * 描述:如果While里面放try / catch,会导致中断失效
 * */
public class CantInterrupt {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            while (num <= 10000){
                if (num % 100 == 0){
                    System.out.println(num+"是能被100整除的数");
                }
                num++;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

打印结果:

我们发现,执行 interrupt 之后,明明已经catchInterruptedException异常,但在线程并没有停止,反而继续执行,这是为什么呢?因为抛出异常后被catch住了,但是循环并没有跳出,不满足跳出循环的条件,会继续执行while循环。

那是不是加上在while()里加上&& !Thread.currentThread().isInterrupted(),在线程中断后,下一次循环开始时判断一下线程是否已被中断就可以了呢?来试一试吧。

/**
 * 描述:如果While里面放try / catch,会导致中断失效
 * */
public class CantInterrupt {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            //while()加一个条件:!Thread.currentThread().isInterrupted()
            while (num <= 10000 && !Thread.currentThread().isInterrupted()){
                if (num % 100 == 0){
                    System.out.println(num+"是能被100整除的数");
                }
                num++;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

线程依然没有停止!!!

原因是java在设计sleep()函数的时候,有这样一个理念,当它一旦响应中断,于是便会把isInterrupted()这个标记位给清除,所以在上面代码while后续检查过程中检查不到任何被中断过的迹象,导致程序不能退出。

如何解决呢?下面有两种方案。

4.1 传递中断

有时候在run()方法中不会一次性把所有业务都写在该方法中,我们可能会调用其他子方法。假设被调用的子方法在某些代码环节可能需要处理InterruptedException 异常,这时通常有两种方法:一是try/catch,二是在方法签名上直接抛出这个异常 throws xxxException

下面先来演示一下非常不好的try/catch,并说一下为什么不好。

4.1.1 举一个使用try-catch的例子:

代码过程是:调用一个通过try/catch处理异常的方法,方法内容是睡眠2秒,启动子线程后,主线程睡眠 1秒后发起中断请求,确保子线程是在throwInMethod方法里的sleep时响应中断。

/**
 * 描述: 最佳实践:catch了interruptedException之后的优先选择:在方法签名中抛出异常
 * 那么在run()方法就会强制要求try/catch
 * */
public class RightWayStopThreadInProd implements Runnable{
    @Override
    public void run() {
        while(true) {
            System.out.println("go");
            throwInMethod();
        }
    }

    private void throwInMethod() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new
                RightWayStopThreadInProd());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

打印结果:

try/catch不好的原因如下

上图明显异常已经打印了,但是不好的是,如果线程继续运行,在茫茫控制台信息中是很难注意到这个异常的,这就导致很难去处理它,所以实际上当有一个线程来打断我们时,我们没有处理好,而且把这个异常给忽略掉了,在实际生产环境中,我们就不一定能感知到这个异常。假设throwInMethod()方法是其他人员写的,我们只是负责调用的话,那遗憾的事情就发生了,别人想中断我们,但我们没有响应,而且可能还毫不知情,因为我们是负责写run()方法的内容,throwInMethod()方法是其他小伙伴负责的,我们并不了解里面的业务逻辑,只是简简单单地调用它,最后的责任却是在我们这里,因为其他线程想中断我们,我们却没有响应中断。

throwInMethod()方法中直接把中断给吞了,什么叫吞了呢?就是在throwInMethod()方法休眠的过程中有一个中断过来,但是它没有做什么处理,只是把它打印出来,没有上报给调用它的方法(run方法),它要做的应该是上报给我们run方法,因为它无法做出更多中断处理,实际应交给我们调用方,交给run方法处理,去决定在调用方法这步代码有异常情况我们应该怎么处理,是该保存日志或其它操作等等,这是我们的责任,而编写throwInMethod()方法的小伙伴的责任绝不是把异常简单的打印出来,自己吞掉,应该上报给我们,把中断的这个信息传给我们。

4.1.2 方法签名上抛出这个异常 throws InterruptedException

目的是让上层方法能感知到这个异常并做出相应的处理!!!

/**
 * 描述: 最佳实践1:catch了interruptedException之后的优先选择:在方法签名中抛出异常
 * 那么在run()方法就会强制要求try/catch
 * */
public class RightWayStopThreadInProd implements Runnable{
    @Override
    public void run() {
        while(true) {
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                //保存日志、停止程序
                System.out.println("保存日志");
                e.printStackTrace();
            }
        }
    }

    private void throwInMethod() throws InterruptedException {
        Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new
                RightWayStopThreadInProd());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

打印结果:

小结: 在被调用的字方法里如果有InterruptedException 异常时,优先选择在方法上抛出异常,即传递中断:。用throws InterruptedException 标记你的方法,不采用try 语句块捕获异常,以便于该异常可以传递到顶层,让run方法可以捕获这一异常,例如:

void subTask() throws InterruptedException{
	sleep(delay);
}
  • 1
  • 2
  • 3

由于run方法内无法抛出checked Exception,即只能用try catch,所以run这个顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增强了代码的健壮性。

4.1.2 不想传递或者无法传递中断:那就选择恢复中断

我们上面说优先处理中断的方法是传递中断,但是在有些情况我们是无法传递的,比如说,我们是作为run()方法的编写者,在run()方法中是不允许抛出异常的,或者有时候我们确实不想在这个方法上抛出异常,就是要自己处理的话,这边也给出了一种对应的方法:恢复中断

恢复中断总体而言就是我们在获取InterruptedException的同时,应该在catch语句中再次调用Thread.currentThread().interrupt(),这样就相当于自己把中断重新设置了一遍,这样一来在后续的执行中依然能检测到刚才发生的这个中断,并且有后续的逻辑继续去处理。

代码演示:

/**
 * 描述: 最佳实践2:在catch子语句中调用Thread.currentThread.interrupt()来恢复中断状态,
 * 以便于在后续的执行中,依然能够检查到刚才发生的中断
 * 回到刚才RightWayStopThreadInProd补上中断,让它跳出
 * */
public class RightWayStopThreadInProd2 implements Runnable{
    @Override
    public void run() {
        while(true) {
            if (Thread.currentThread().isInterrupted()){
                System.out.println("Interrupted,程序运行结束");
                break;
            }
            reInterrupt();
        }
    }

    private void reInterrupt() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new
                RightWayStopThreadInProd2());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

打印结果:

所以:如果不想或无法传递InterruptedException(例如用run方法的时候,就不让该方法throws InterruptedException),那么应该选择在catch 子句中调用Thread.currentThread().interrupt() 来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断,正常退出。

可以响应中断的方法总结列表:

Object.wait() / wait(long) / wait(long, int)
Thread.sleep(long) / sleep(long, int)
Thread.join() / join( long) / join(long, int)
java.util.concurrent.BlockingQueue.take() / put (E)
java.util.concurrent.locks.Lock.lockInterruptibly()
java.util.concurrent.CountDownLatch.await
java.util.concurrent.CyclicBarrier.await
java.util.concurrent.Exchanger.exchange(v)
java.nio.channels.InterruptibleChannel相关方法
java.nio.channels.Selector的相关方法


unchecked Exception 和 checked Exception的概念:

java 异常体系 Throwable 分为两类:Error和Exception。

Error是代码层面无法处理的系统之类的问题;Exception分为 RuntimeException、IOException等,RuntimeException 和 Error 属于 unchecked Exception(不受检查异常),因为编译器无法对这类异常提前预测。而Exception中除了 RuntimeException 以外的其他 Exception 都属于 checked Exception(受检查异常),因为可以被编译器提前预知,并对可能出现的异常执行对应的代码处理,比如 try/catch、throws等;

image-20230525131753531


三、错误的停止方法

1. 被弃用的stop,suspend和resume方法

用stop()来停止线程,会导致线程运行一半突然停止,没办法完成一个基本单位的操作(一个连队),会造成脏数据(有的连队多领取少领取装备)。

public class StopThread implements Runnable {

    @Override
    public void run() {
        //模拟指挥军队:一共有5个连队,每个连队10人,以连队为单位,发放武器弹药,叫到号的士兵前去领取
        for (int i = 0; i < 5; i++) {
            System.out.println("连队" + i + "开始领取武器");
            for (int j = 0; j < 10; j++) {
                System.out.println(j);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("连队"+i+"已经领取完毕");
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new StopThread());
        thread.start();
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.stop();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

打印结果:

上面可以看到,stop太强制,连队1的人员还没有领取完就停止了,也就是说stop会导致原来的逻辑没完整做完就停止了。

2. 用volatile设置boolean标记位

(1) 部分场景可行
public class WrongWayVolatile implements Runnable {

    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    System.out.println(num + "是能被100整除的数");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatile r = new WrongWayVolatile();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(5000);
        r.canceled = true;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

打印结果:

这种情况下通过volitile来中断线程是可行的。

(2) 不可行的场景

代码说明:下面的例子,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费

// 主类:
public class WrongWayVolatileCantStop {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take()+"被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");

        //一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况生产者还是会处于阻塞
        producer.canceled=true;
        System.out.println(producer.canceled);
    }
}
// 生产者:
class Producer implements Runnable {

    public volatile boolean canceled = false;

    BlockingQueue storage;

    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }


    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    storage.put(num);
                    System.out.println(num + "是100的倍数,被放到仓库中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }
}
// 消费者:
class Consumer {

    BlockingQueue storage;

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

    public boolean needMoreNums() {
        if (Math.random() > 0.95) {
            return false;
        }
        return true;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

打印结果:

我们发现volatile设置为了true,但是生产者并没有停下来。

为什么说用volatile停止线程不够全面?

  • 这种做法是错误的,或者说是不够全面的,在某些情况下虽然可用,但是某些情况下有严重问题。
  • 此方法错误的原因在于,如果我们遇到了线程长时间阻塞(这是一种很常见的情况,例如生产者消费者模式中就存在这样的情况),就没办法及时唤醒它,或者永远都无法唤醒该线程,而 interrupt设计之初就是把wait等长期阻塞作为一种特殊情况考虑在内了,我们应该用 interrupt来停止线程。
(3) 改进(2)中的代码

interrupt中断来修复刚才的无尽等待问题

// main函数:
public class WrongWayVolatileFixed {

    public static void main(String[] args) throws InterruptedException {

        WrongWayVolatileFixed body = new WrongWayVolatileFixed();
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        Producer producer = body.new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = body.new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take() + "被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");
        producerThread.interrupt();
        System.out.println(producer.isCanceled);
    }
// 生产者:
class Producer implements Runnable {

        BlockingQueue storage;

        public Producer(BlockingQueue storage) {
            this.storage = storage;
        }
        @Override
        public void run() {
            int num = 0;
            try {
                while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        storage.put(num);
                        System.out.println(num + "是100的倍数,被放到仓库中了。");
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生产者结束运行");
            }
        }
    }
// 消费者:
class Consumer {

        BlockingQueue storage;

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

        public boolean needMoreNums() {
            if (Math.random() > 0.95) {
                return false;
            }
            return true;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

打印结果:

使用interrupt,程序正常中断了。

(4) 判断是否已被中断的相关方法
  • static boolean interrupted() 判断当前线程是否被中断,调用后会把中断线程的标记直接设为false,即清除中断标记
  • boolean isInterrupted() 判断当前线程是否被中断,不会清除中断标记

注意 static boolean interrupted() 方法的目标对象是“当前线程”,而不管本方法来自于哪个实例对象:

public class RightWayInterrupted {

    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(() -> {
            for (; ; ) {
            }
        });

        // 启动线程
        threadOne.start();
        //设置中断标志
        threadOne.interrupt();
        //获取中断标志
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        //获取中断标志并重置:threadOne.interrupted()虽然是由threadOne线程发出的,但是实际执行的对象还是主线程
        System.out.println("isInterrupted: " + threadOne.interrupted());
        //获取中断标志并重直
        System.out.println("isInterrupted: " + Thread.interrupted());
        //获取中断标志
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        threadOne.join();
        System.out.println("Main thread is over.");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

打印结果:

interrupted()是静态方法,无论是对象调用还是类调用判断的都是主线程的中断标志,所以应该都是 false。

四、总结

1. 停止线程使用 interrupt 方法

使用interrupt方法请求中断,而不是强制终止线程,是合作机制。这样,被请求中断的线程可以自主决定,处理自己的逻辑。好处是,可以保证数据安全,来得及清理,能够保证数据完整性。

2. 若让interrupt方法起效,需要多方面的配合使用

  • 请求方发出中断请求

  • 被停止方要在每次循环中或适当的时候检查中断信号,并在可能抛出InterruptException的时候处理这个信号

  • 如果线程中响应中断的是子方法,子方法被外层方法调用,有两种响应中断的最佳方案:

    • 1)传递中断、即优先在子方法层向上抛出异常,将中断信号传给run方法,在run方法层处理中断信号逻辑
    • 2)恢复中断、即子方法收到中断信号后,再次设置中断状态。

3. 如果不用interrupt,其他方法会有一定的弊端与后果

  • stop会突然停止线程,线程来不及处理剩下的数据,会导致数据不完整

  • suspend等方法会使线程挂起,不会破坏对象,抱着锁阻塞,会导致死锁

  • 用volatile设置boolean标记位无法处理长时间阻塞的情况,导致线程无法停止

如果线程阻塞是由于调用了wait(),sleep()或join()方法,你可以中断线程,通过抛出 InterruptedException异常来唤醒该线程。

4. 无法响应中断时如何停止线程,即如何处理不可中断的阻塞

需要根据不同的类调用不同的方法。

如果线程阻塞是由于调用了 wait(),sleep() 或 join() 方法,你可以中断线程,通过抛出 InterruptedException 异常来从阻塞中唤醒该线程。
但是对于不能响应InterruptedException的阻塞,很遗憾,并没有一个通用的解决方案。
但是我们可以利用特定的其它的可以响应中断的方法,比如ReentrantLock.lockInterruptibly(),比如关闭套接字使线程立即返回等方法来达到目的。
答案有很多种,因为有很多原因会造成线程阻塞,所以针对不同情况,唤起的方法也不同。

总结就是说如果不支持响应中断,就要用特定方法来唤起,没有万能药。

文章来源:深度解析线程的正确停止方法

个人微信:CaiBaoDeCai

微信公众号名称:Java知者

微信公众号 ID: JavaZhiZhe

谢谢关注!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/1004240
推荐阅读
相关标签
  

闽ICP备14008679号