Java并发基础学习(二)——线程的停止和中断

前言

上一篇博客简单介绍了线程启动的方式,这一篇博客打算介绍一下如何停止线程,Java中停止线程相对来说就比较麻烦了,如何正确的停止线程其实也是一个比较常见的面试考题,需要详细总结一下。

线程停止的原理

Java中线程的停止并不是像关闭一个开关一样,直接停止线程,Java中的线程停止原理有点类似于计算机组成原理中对中断的处理,首先关闭中断标志位,让后响应中断,然后处理中断。

Java中线程的处理也可以看成大致的这个过程——**线程本身响应外部的中断通知,然后将中断标志位复位,但是什么之后线程停止,由线程本身自己决定。**这也是Java中最好的停止线程的方式。

如何正确停止线程

其实不同状态的线程,对中断信号的响应是不同的,因此在不同的线程逻辑的停止方式也有细微的差异。这里简单总结一下。

正常运行状态的线程停止

实例代码

/**
 * autor:liman
 * createtime:2021/9/9
 * comment: run方法内没有sleep或wait方法的时候停止线程
 */
public class RightWayStopThreadWithoutSleep implements Runnable {
    
    

    @Override
    public void run() {
    
    
        int num = 0;
        //while判断的时候,调用当前线程的isInterrupted判断中断标志位是否有中断的信号
        while (!Thread.currentThread().isInterrupted()
                && num <= Integer.MAX_VALUE / 2) {
    
    
            if (num % 10000 == 0) {
    
    
                System.out.println(num + " is 10000 的倍数");
            }
            num++;
        }
        System.out.println("任务运行结束");
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
        Thread.sleep(1000);//主线程睡眠1秒
        thread.interrupt();//给子线程发出中断信号
    }
}

正常的运行结果如下:

请添加图片描述

sleep状态的线程停止

如果目标线程本身是在sleep的阻塞状态,这个时候目标线程响应中断的方式也是有些许差异的

/**
 * autor:liman
 * createtime:2021/9/9
 * comment: run方法内有sleep或wait方法的时候停止线程
 */
public class RightWayStopThreadWithSleep {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable runnable = ()->{
    
    
            int num = 0;
            while(num<=300 && !Thread.currentThread().isInterrupted()){
    
    
                if(num %100 ==0){
    
    
                    System.out.println(num + " is 100的倍数");
                }
                num++;
            }
            try {
    
    
                Thread.sleep(5000);//执行完运算之后,进入sleep的阻塞状态
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(1000);//主线程睡眠1秒
        thread.interrupt();//给子线程发出中断信号
    }
}

运行结果

请添加图片描述

在sleep阻塞状态中的线程,响应中断的方式比较特殊,是直接抛出异常。 这也是Java为什么强制需要我们对sleep抛出的异常进行处理的原因。目标线程在sleep的时候,依旧能响应中断。

需要说明一下的是,上面的实例中,每次循环的时候,在while的时候,都判断了一下中断的标志位,如果sleep在while循环体中(也就是每次循环sleep一下),这种情况下while循环的开头,是不需要判断中断标志位的

/**
 * autor:liman
 * createtime:2021/9/9
 * comment: 如果在执行过程中,每次执行都会sleep或者wait等待,则不需要每次迭代中都判断中断标记
 */
public class RightWayStopThreadWithSleepEveryLoop {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable runnable = () -> {
    
    
            try {
    
    
                int num = 0;
                while (num <= 10000 ) {
    
    //循环体中有sleep,外部可以不用判断中断标志位
                    if (num % 100 == 0) {
    
    
                        System.out.println(num + " is 100的倍数");
                    }
                    num++;
                    Thread.sleep(10);//执行完运算之后,等待1秒
                }
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

还有一个需要注意的是,sleep响应中断之后,中断标志位会复位(这一点和计算机组成原理中的中断响应太像了),将上述代码中while循环外的try-catch放入到循环体内部,即可看到不同的效果

/**
 * autor:liman
 * createtime:2021/9/9
 * comment:while里头放try/catch,则会导致中断失效
 */
public class CanInterrupt {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable runnable = ()->{
    
    
            int num = 0;
            while(num <= 10000 && !Thread.currentThread().isInterrupted()){
    
    //即使这里判断中断标志位也并不能停止线程
                if(num % 100 == 0){
    
    
                    System.out.println(num + " is 100 的倍数");
                }
                num++;
                try {
    
    
                    Thread.sleep(10);//sleep响应中断,catch处理之后,会复位中断标志位
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();//这里响应中断之后,中断标志位被复位,线程并不能被停止
                }
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

运行结果如以下动图所示:

请添加图片描述

在响应中断之后,目标线程并没有终止,而是继续欢快的运行,所以在停止sleep阻塞状态中的线程的时候,需要注意中断标志位的复位问题

较好的停止线程的方式

针对sleep本身对中断标志位清除的问题,简单总结了一下正确停止线程的方法。

1、子方法的中断异常需要抛出

先说问题,如果将中断的异常,在子方法中进行处理,则相关异常在处理完成之后,中断标志位复位。并不能完成中断的

public class ProdRightWayStopThread implements Runnable{
    
    
    @Override
    public void run() {
    
    
        while(true) {
    
    
            System.out.println("running");
            catchInterruptInMethod();
        }
    }

    //目标线程调用的子方法
    private void catchInterruptInMethod() {
    
    
		try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    //子方法内部处理中断的异常。
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(new ProdRightWayStopThread());
        thread.start();
        Thread.sleep(900);
        thread.interrupt();//通知目标线程,中断。
    }
}

运行结果和上面的代码参不多,一直running,输出一行异常信息之后,继续running,如果这是生产,则相关异常的日志会淹没在海量的日志数据中……

因此较好的方式,是强制在run方法中处理子方法的相关异常(不只是InterruptedException)

/**
 * autor:liman
 * createtime:2021/9/11
 * comment:生产中正确的停止线程的方式之一 强制要求子方法抛出InterruptedException的异常
 */
public class ProdRightWayStopThread implements Runnable{
    
    
    @Override
    public void run() {
    
    
        while(true) {
    
    
            System.out.println("running");
            try {
    
    
                catchInterruptInMethod();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();//这里可以做相关的异常处理。
                return;
            }
        }
    }

    private void catchInterruptInMethod() throws InterruptedException {
    
    
        //线程的调用方法中,sleep的时候针对中断异常的处理并没有抛出
            //sleep模拟子线程在运行
            Thread.sleep(1000);
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(new ProdRightWayStopThread());
        thread.start();
        Thread.sleep(900);
        thread.interrupt();
    }
}

2、恢复中断标志位

如果子方法确实无法抛出中断的相关异常,可以通过恢复中断标志位来进行,还是基于上述的实例进行优化

/**
 * autor:liman
 * createtime:2021/9/11
 * comment:生产中正确的停止线程的方式之一:在子方法中重新通知中断,以便于父方法中能够检查到刚才发生了中断。
 */
@Slf4j
public class ProdRightWayStopThreadResetInterrupt implements Runnable {
    
    
    @Override
    public void run() {
    
    
        while (true) {
    
    
            if(Thread.currentThread().isInterrupted()){
    
    
                log.info("thread is interrupted,线程中断,运行终止");
                break;
            }
            System.out.println("running");
            catchInterruptInMethod();
        }
    }

    private void catchInterruptInMethod() {
    
    
        //线程的调用方法中,sleep的时候针对中断异常的处理并没有抛出
        //sleep模拟子线程在运行
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            log.error("收到中断信号,处理中断,信息为:{}",e);
            //重新通知中断,在父run方法中,正常响应中断即可。
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(new ProdRightWayStopThreadResetInterrupt());
        thread.start();
        Thread.sleep(10000);
        thread.interrupt();
    }
}

总的来说线程停止较好的方式就两种,无非就是将中断异常抛出,或者重新开中断。

几种错误的停止方式

1、stop,suspend和resume

这几种方式其实已经被弃用了,自然是错误的,相比之前介绍的几种比较优雅的方式,这种方式暴力的多,并且会造成脏数据,这些脏数据会对程序的后续运行产生很大的问题,这个就不做具体的实例了。suspend是带着锁进入到阻塞,容易造成死锁,因此被丢弃

2、volatile修饰的标记位

关于采用volatile修饰的变量,来停止线程,这个在有些博客或者书籍上归纳为正确的停止方式,但是在目标线程阻塞的时候,这种方式并不能正确停止线程

/**
 * autor:liman
 * createtime:2021/9/11
 * comment: volatile的局限性,无法停止线程的实例
 * 当线程阻塞的时候,volatile无法停止线程
 * 生产者生产数据很快,消费者消费数据很慢,所以阻塞队列满了之后,生产者会阻塞,这种情况 volatile是不生效的
 */
@Slf4j
public class WrongWayVolatileCannotStop{
    
    

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

        ArrayBlockingQueue dataQueue = new ArrayBlockingQueue(10);
        Producer producer = new Producer(dataQueue);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(10000);//让生产者生产数据

        Consumer consumer = new Consumer(dataQueue);
        while(consumer.canConsumer(dataQueue)){
    
    
            log.info("数据:{} 被消费者消费了",consumer.dataQueue.take());
            Thread.sleep(100);//模拟消费者对获取到的数据进行处理
        }
        log.info("消费者不再需要更多数据,给生产者发送中断信号");

        producer.canceled = true;//消费者数据消费完成,停止生产者
    }


}

@Slf4j
class Producer implements Runnable{
    
    

    public volatile boolean canceled = false;

    BlockingQueue dataQueue;

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

    @Override
    public void run() {
    
    
        int num = 0;
        try {
    
    
            while (num <= 100000 && !canceled) {
    
    
                if (num % 100 == 0) {
    
    
                    dataQueue.put(num);//数据放入队列
                    log.info("num : {} 是100的倍数", num);
                }
                num++;
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            log.info("【生产者】运行结束");
        }
    }
}

@Slf4j
class Consumer{
    
    

    BlockingQueue dataQueue;

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

    /**
     * 判断是否能进一步的消费数据
     * @param dataQueue
     * @return
     */
    public boolean canConsumer(BlockingQueue dataQueue){
    
    
        //if(dataQueue.size()<=0){
    
    
        if(Math.random()>0.95){
    
    //消费者不需要太多数据,不一定会消费所有生产者的数据
            return false;
        }
        return true;
    }
}

上述代码有点复杂,是模拟了一个简单的生产者和消费者的简单实例,但是消费者消费数据比较慢,由于消费者消费数据不及时,导致生产者最终会进入到阻塞状态,在队列数据满了之后,生产者阻塞在dataQueue.put()方法中了,这个时候,生产者是无法判断代码中volatile修饰的标志位变量的,因此依旧阻塞,无法停止。运行结果如下:

请添加图片描述

从运行结果来看,线程并没有终止。因此这种情况volatile并不适用,如果要正确停止上述线程,用interrupt的方法即可。

interrupt相关方法

有很多方法与interrupt方法比较相似,具体如下

1static boolean interrupted()

2boolean isInterrupted()

3Thread.interrupted()的目的对象

第一个静态方法(注意,这个方法不是interrupt),判断线程是否中断,返回判断结果之后,会将线程的中断状态置为false

第二个方法,判断线程是否中断,返回判断结果之后,不会将线程的中断状态置为false

第三个方法,需要注意的是,这个其实就是第一个方法,只是调用的时候不是通过具体的对象来调用,而是通过Thread类进行调用。

/**
 * autor:liman
 * createtime:2021/9/11
 * comment:各种interrupt关联方法的结果【
 */
public class RightWayInterruptMethod {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    

        Thread threadOne = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                for (; ; ) {
    
    
                }
            }
        });

        // 启动线程
        threadOne.start();
        //设置中断标志
        threadOne.interrupt();
        //获取中断标志
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        //输出当前线程的中断标志结果(当前线程是main,不管调用这个方法的对象是什么,因为这个方法是静态方法)
        System.out.println("isInterrupted: " + threadOne.interrupted());
        //输出当前线程的中断标志结果(当前线程是main)
        System.out.println("isInterrupted: " + Thread.interrupted());
        //获取中断标志
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        threadOne.join();
        System.out.println("Main thread is over.");
    }
}

运行结果

请添加图片描述

第二行和第三行输出结果,不论调用interrupted方法的是对象还是Thread类,其判断结果都是当前线程的中断标志位的结果。而当前的运行线程是main线程,故而是false。

常见的面试问题

1、如何停止线程

用interrupt来操作,而不是所谓的stop和volatile等方式,interrupt本身在一定程度上可以保证数据安全,要想达到正确停止线程的效果,除了简单发出中断信号还不行,还需要目标线程进行配合,通知目标线程对中断异常也要做一个正取处理,不能在子方法中将静默处理,如果在子方法中处理也要重新开中断。volatile在一定场景下可以正常停止,但是在目标线程阻塞的时候,volatile变量停止线程的操作,就不那么优秀了。

2、如何处理不可中断的阻塞

interrupt方法并不是万能的,在目标线程进行socket io操作的时候,线程长期处于阻塞状态,但是这种阻塞并没有一个通用的解决方案,这种时候,我们就需要设置IO超时的时间,让线程能做到可以响应中断。

可以响应中断的方法

除了sleep方法,还有些方法,目标线程在执行该指定方法的时候,虽然不是运行状态,但是也可以响应中断。这些方法如下:

1Object.wait()/wait(long)/wait(long,int)
2Thread.sleep(long)/sleep(long,int)
3Thread.join()/join(long)/join(long,int)
4java.util.concurrent.BlockingQueue.take()/put(E)
5java.util.concurrent.locks.Lock.lockInterruptibly()
6java.util.concurrent.CountDownLatch.await()
7java.util.concurrent.CyclicBarrier.await()
8java.util.concurrent.Exchanger.exchange(V)
9java.nio.channels.InterruptibleChannel相关方法
10java.nio.channels.Selector的相关方法

总结

关于停止,掌握上面梳理的内容,应该差不多了,后续总结线程的几种状态

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/120660007
今日推荐