zoukankan      html  css  js  c++  java
  • Java多线程系列---“基础篇”09之 interrupt()和线程终止方式

    转自:https://www.cnblogs.com/swiftma/p/6477189.html  & https://blog.csdn.net/kusedexingfu/article/details/72629371 && https://blog.csdn.net/hj7jay/article/details/53462553(含部分修改)

    本节主要讨论一个问题,如何在Java中取消或关闭一个线程?

    取消/关闭的场景

    我们知道,通过线程的start方法启动一个线程后,线程开始执行run方法,run方法运行结束后线程退出,那为什么还需要结束一个线程呢?有多种情况,比如说:

    • 很多线程的运行模式是死循环,比如在生产者/消费者模式中,消费者主体就是一个死循环,它不停的从队列中接受任务,执行任务,在停止程序时,我们需要一种"优雅"的方法以关闭该线程。
    • 在一些图形用户界面程序中,线程是用户启动的,完成一些任务,比如从远程服务器上下载一个文件,在下载过程中,用户可能会希望取消该任务。
    • 在一些场景中,比如从第三方服务器查询一个结果,我们希望在限定的时间内得到结果,如果得不到,我们会希望取消该任务。
    • 有时,我们会启动多个线程做同一件事,比如类似抢火车票,我们可能会让多个好友帮忙从多个渠道买火车票,只要有一个渠道买到了,我们会通知取消其他渠道。

    取消/关闭的机制

    Java的Thread类定义了如下方法:

    public final void stop()

    这个方法看上去就可以停止线程,但这个方法被标记为了过时,简单的说,我们不应该使用它,可以忽略它。

    在Java中,停止一个线程的主要机制是中断,中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出,本节我们主要就是来理解Java的中断机制。

    Thread类定义了如下关于中断的方法:

    public boolean isInterrupted()
    public void interrupt()
    public static boolean interrupted() 

    这三个方法名字类似,比较容易混淆,我们解释一下。isInterrupted()和interrupt()是实例方法,调用它们需要通过线程对象,interrupted()是静态方法,实际会调用Thread.currentThread()操作当前线程。

    每个线程都有一个标志位,表示该线程是否被中断了。

    • isInterrupted:就是返回对应线程的中断标志位是否为true。
    • interrupted:返回当前线程的中断标志位是否为true,但它还有一个重要的副作用,就是清空中断标志位,也就是说,连续两次调用interrupted(),第一次返回的结果为true,第二次一般就是false (除非同时又发生了一次中断)。
    • interrupt:表示中断对应的线程

    中断具体意味着什么呢?下面我们进一步来说明。

    线程对中断的反应

    interrupt()对线程的影响与线程的状态和在进行的IO操作有关,我们先主要考虑线程的状态:

    • RUNNABLE:线程在运行或具备运行条件只是在等待操作系统调度
    • WAITING/TIMED_WAITING:线程在等待某个条件或超时
    • BLOCKED:线程在等待锁,试图进入同步块
    • NEW/TERMINATED:线程还未启动或已结束

    RUNNABLE

    如果线程在运行中,且没有执行IO操作,interrupt()只是会设置线程的中断标志位,没有任何其它作用。线程应该在运行过程中合适的位置检查中断标志位,比如说,如果主体代码是一个循环,可以在循环开始处进行检查,如下所示:

    public class InterruptRunnableDemo extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello");
            }
            System.out.println("done ");
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new InterruptRunnableDemo();
            thread.start();
            Thread.sleep(1000);
            thread.interrupt();
        }
    }
    hello
    hello
    hello
    hello
    done 

    WAITING/TIMED_WAITING

    线程执行如下方法会进入WAITING状态:

    public final void join() throws InterruptedException
    public final void wait() throws InterruptedException

    执行如下方法会进入TIMED_WAITING状态:

    public final native void wait(long timeout) throws InterruptedException;
    public static native void sleep(long millis) throws InterruptedException;
    public final synchronized void join(long millis) throws InterruptedException

    在这些状态时,对线程对象调用interrupt()会使得该线程抛出InterruptedException,需要注意的是,抛出异常后,中断标志位会被清空,而不是被设置。比如说,执行如下代码:

    复制代码
    Thread t = new Thread (){
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println(isInterrupted());
            }
        }        
    };
    t.start();
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
    }
    t.interrupt();
    复制代码

    程序的输出为false。

    InterruptedException是一个受检异常,线程必须进行处理。我们在异常处理中介绍过,处理异常的基本思路是,如果你知道怎么处理,就进行处理,如果不知道,就应该向上传递,通常情况下,你不应该做的是,捕获异常然后忽略。

    捕获到InterruptedException,通常表示希望结束该线程,线程大概有两种处理方式:

    1. 向上传递该异常,这使得该方法也变成了一个可中断的方法,需要调用者进行处理。
    2. 有些情况,不能向上传递异常,比如Thread的run方法,它的声明是固定的,不能抛出任何受检异常,这时,应该捕获异常,进行合适的清理操作,清理后,一般应该调用Thread的interrupt方法设置中断标志位,使得其他代码有办法知道它发生了中断。

    第一种方式的示例代码如下:

    public void interruptibleMethod() throws InterruptedException{
        // ... 包含wait, join 或 sleep 方法
        Thread.sleep(1000);
    }

    第二种方式的示例代码如下:

    复制代码
    public class InterruptWaitingDemo extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 模拟任务代码
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    // ... 清理操作
                    // 重设中断标志位
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println(isInterrupted());
        }
    
        public static void main(String[] args) {
            InterruptWaitingDemo thread = new InterruptWaitingDemo();
            thread.start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
            thread.interrupt();
        }
    }
    复制代码

    BLOCKED

    如果线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正"中断"。我们看段代码:

    复制代码
    public class InterruptSynchronizedDemo {
        private static Object lock = new Object();
    
        private static class A extends Thread {
            @Override
            public void run() {
                synchronized (lock) {
                    while (!Thread.currentThread().isInterrupted()) {
                    }
                }
                System.out.println("exit");
            }
        }
    
        public static void test() throws InterruptedException {
            synchronized (lock) {
                A a = new A();
                a.start();
                Thread.sleep(1000);
    
                a.interrupt();
                a.join();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            test();
        }
    }
    复制代码

    test方法在持有锁lock的情况下启动线程a,而线程a也去尝试获得锁lock,所以会进入锁等待队列,随后test调用线程a的interrupt方法并等待线程线程a结束,线程a会结束吗?不会,interrupt方法只会设置线程的中断标志,而并不会使它从锁等待队列中出来。

    我们稍微修改下代码,去掉test方法中的最后一行a.join,即变为:

    复制代码
    public static void test() throws InterruptedException {
        synchronized (lock) {
            A a = new A();
            a.start();
            Thread.sleep(1000);
    
            a.interrupt();
        }
    }
    复制代码

    这时,程序就会退出。为什么呢?因为主线程不再等待线程a结束,释放锁lock后,线程a会获得锁,然后检测到发生了中断,所以会退出。

    在使用synchronized关键字获取锁的过程中不响应中断请求,这是synchronized的局限性。如果这对程序是一个问题,应该使用显式锁,后面章节我们会介绍显式锁Lock接口,它支持以响应中断的方式获取锁。

    NEW/TERMINATE

    如果线程尚未启动(NEW),或者已经结束(TERMINATED),则调用interrupt()对它没有任何效果,中断标志位也不会被设置。比如说,以下代码的输出都是false。

    复制代码
    public class InterruptNotAliveDemo {
        private static class A extends Thread {
            @Override
            public void run() {
            }
        }
    
        public static void test() throws InterruptedException {
            A a = new A();
            a.interrupt();
            System.out.println(a.isInterrupted());
    
            a.start();
            Thread.sleep(100);
            a.interrupt();
            System.out.println(a.isInterrupted());
        }
    
        public static void main(String[] args) throws InterruptedException {
            test();
        }
    }
    复制代码

    IO操作

    如果线程在等待IO操作,尤其是网络IO,则会有一些特殊的处理,我们没有介绍过网络,这里只是简单介绍下。

    • 如果IO通道是可中断的,即实现了InterruptibleChannel接口,则线程的中断标志位会被设置,同时,线程会收到异常ClosedByInterruptException。
    • 如果线程阻塞于Selector调用,则线程的中断标志位会被设置,同时,阻塞的调用会立即返回。

    我们重点介绍另一种情况,InputStream的read调用,该操作是不可中断的,如果流中没有数据,read会阻塞 (但线程状态依然是RUNNABLE),且不响应interrupt(),与synchronized类似,调用interrupt()只会设置线程的中断标志,而不会真正"中断"它,我们看段代码。

    复制代码
    public class InterruptReadDemo {
        private static class A extends Thread {
            @Override
            public void run() {
                while(!Thread.currentThread().isInterrupted()){
                    try {
                        System.out.println(System.in.read());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }    
                }
                System.out.println("exit");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            A t = new A();
            t.start();
            Thread.sleep(100);
    
            t.interrupt();
        }
    }
    复制代码

    线程t启动后调用System.in.read()从标准输入读入一个字符,不要输入任何字符,我们会看到,调用interrupt()不会中断read(),线程会一直运行。

    不过,有一个办法可以中断read()调用,那就是调用流的close方法,我们将代码改为:

    复制代码
    public class InterruptReadDemo {
        private static class A extends Thread {
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        System.out.println(System.in.read());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("exit");
            }
    
            public void cancel() {
                try {
                    System.in.close();
                } catch (IOException e) {
                }
                interrupt();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            A t = new A();
            t.start();
            Thread.sleep(100);
    
            t.cancel();
        }
    }
    复制代码

    我们给线程定义了一个cancel方法,在该方法中,调用了流的close方法,同时调用了interrupt方法,这次,程序会输出:

    -1
    exit

    也就是说,调用close方法后,read方法会返回,返回值为-1,表示流结束。

    如何正确地取消/关闭线程

    以上,我们可以看出,interrupt方法不一定会真正"中断"线程,它只是一种协作机制,如果不明白线程在做什么,不应该贸然的调用线程的interrupt方法,以为这样就能取消线程。

    对于以线程提供服务的程序模块而言,它应该封装取消/关闭操作,提供单独的取消/关闭方法给调用者,类似于InterruptReadDemo中演示的cancel方法,外部调用者应该调用这些方法而不是直接调用interrupt。

    Java并发库的一些代码就提供了单独的取消/关闭方法,比如说,Future接口提供了如下方法以取消任务: 

    boolean cancel(boolean mayInterruptIfRunning);

    再比如,ExecutorService提供了如下两个关闭方法:

    void shutdown();
    List<Runnable> shutdownNow();

    Future和ExecutorService的API文档对这些方法都进行了详细说明,这是我们应该学习的方式。关于这两个接口,我们后续章节介绍。

    小结

    本节主要介绍了在Java中如何取消/关闭线程,主要依赖的技术是中断,但它是一种协作机制,不会强迫终止线程,我们介绍了线程在不同状态和IO操作时对中断的反应,作为线程的实现者,应该提供明确的取消/关闭方法,并用文档描述清楚其行为,作为线程的调用者,应该使用其取消/关闭方法,而不是贸然调用interrupt。

    补充:

    interrupt、interrupted 、isInterrupted区别(important)

    1. interrupt
    interrupt方法用于中断线程。调用该方法的线程的状态为将被置为"中断"状态。
    注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态并做处理。一旦线程的中断状态位被置为“中断”,就会抛出interruptedException异常。

    2. interrupted 和 isInterrupted

    interrupted实现:

    public static boolean interrupted() {
            return currentThread().isInterrupted(true);
        }
    该方法就是直接调用当前线程的isInterrupted(true)方法。
    isInterrupted实现:
    public boolean isInterrupted() {
            return isInterrupted(false);
        }

    这两个方法有两个主要区别:
    interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A线程中去调用B线程对象的isInterrupted方法。)
    这两个方法最终都会调用同一个方法,只不过参数一个是true,一个是false;
    第二个区别主要体现在调用的方法的参数上,让我们来看一看这个参数是什么含义
    先来看一看被调用的方法 isInterrupted(boolean arg)的定义:

    private native boolean isInterrupted(boolean ClearInterrupted);

    原来这是一个本地方法,看不到源码。通过参数名我们就能知道,这个参数代表是否要清除状态位。
    如果这个参数为true,说明返回线程的状态位后,要清掉原来的状态位。这个参数为false,就是直接返回线程的状态位。

    也就是interrupted返回线程的状态位,又清空了状态位。isInterrupted只是返回了线程的状态位。

    以下程序看这三个方法的区别:

    public class Test {
        public static void main(String[] args) {
            
            System.out.println("isInterrupted :"  + Thread.currentThread().isInterrupted());//当前线程没有被中断,返回false;
            Thread.currentThread().interrupt();//中断了该线程,但是该线程仍会继续执行,该方法只是将线程的状态设置成“中断”状态
            System.out.println("isInterrupted :" + Thread.currentThread().isInterrupted());//interrupt()执行后,线程被设置成中断状态,返回true
            System.out.println("interrupted :" + Thread.interrupted());//返回了中断状态:true,又清掉了状态位。
            System.out.println("isInterrupted :" + Thread.currentThread().isInterrupted());//由于interrupted()清掉了状态位,所以返回false;
            System.out.println("interrupted :" + Thread.interrupted());//返回false
            System.out.println("isInterrupted :" + Thread.currentThread().isInterrupted());//false
        }
    }
    isInterrupted :false
    isInterrupted :true
    interrupted :true
    isInterrupted :false
    interrupted :false
    isInterrupted :false

    那么问题来了:为什么要在抛出InterruptedException的时候清除掉中断状态呢?
    这个问题没有找到官方的解释,估计只有Java设计者们才能回答了。但这里的解释似乎比较合理:一个中断应该只被处理一次(你catch了这个InterruptedException,说明你能处理这个异常,你不希望上层调用者看到这个中断)。
    比如:

    示例1:

    public class Interrupt {
        public static void main(String[] args) throws Exception {
            Thread t = new Thread(new Worker());
            t.start();
            
            Thread.sleep(200);
            t.interrupt();
            
            System.out.println("Main thread stopped.");
        }
        
        public static class Worker implements Runnable {
            public void run() {
                System.out.println("Worker started.");
                
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {//会清除中断标志
                    System.out.println("Worker IsInterrupted: " + 
                            Thread.currentThread().isInterrupted());
                }
                
                System.out.println("Worker stopped.");
            }
        }
    }

    内容很简答:主线程main启动了一个子线程Worker,然后让worker睡500ms,而main睡200ms,之后main调用worker线程的interrupt方法去中断worker,worker被中断后打印中断的状态。下面是执行结果:

    Worker started.
    Main thread stopped.
    Worker IsInterrupted: false
    Worker stopped

    Worker明明已经被中断,而isInterrupted()方法竟然返回了false,为什么呢?

    可以查看抛出InterruptedException方法的JavaDoc(或源代码),于是我查看了Thread.sleep方法的文档,doc中是这样描述这个InterruptedException异常的:

    InterruptedException - if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.

    注意到后面这句“当抛出这个异常的时候,中断状态已被清除”。所以isInterrupted()方法应该返回false。可是有的时候,我们需要isInterrupted这个方法返回true,怎么办呢?这里就要先说说interrupt, interrupted和isInterrupted的区别了:

    interrupt方法是用于中断线程的,调用该方法的线程的状态将被置为"中断"状态。注意:线程中断仅仅是设置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出InterruptedException的方法,比如这里的sleep,以及Object.wait等方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
    示例2修改:

    public class Interrupt  {
        public static void main(String[] args) throws Exception {
            Thread t = new Thread(new Worker());
            t.start();
            
            Thread.sleep(200);
            t.interrupt();
            
            System.out.println("Main thread stopped.");
        }
        
        public static class Worker implements Runnable {
            public void run() {
                System.out.println("Worker started.");
                
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread curr = Thread.currentThread();
                    //再次调用interrupt方法中断自己,将中断状态设置为“中断”
                    curr.interrupt();
                    System.out.println("Worker IsInterrupted: " + curr.isInterrupted());
                    System.out.println("Worker IsInterrupted: " + curr.isInterrupted());
                    System.out.println("Static Call: " + Thread.interrupted());//clear status
                    System.out.println("---------After Interrupt Status Cleared----------");
                    System.out.println("Static Call: " + Thread.interrupted());
                    System.out.println("Worker IsInterrupted: " + curr.isInterrupted());
                    System.out.println("Worker IsInterrupted: " + curr.isInterrupted());
                }
                
                System.out.println("Worker stopped.");
            }
        }
    }
    Worker started.
    Main thread stopped.
    Worker IsInterrupted: true
    Worker IsInterrupted: true
    Static Call: true
    ---------After Interrupt Status Cleared----------
    Static Call: false
    Worker IsInterrupted: false
    Worker IsInterrupted: false
    Worker stopped.
  • 相关阅读:
    结对四则运算04—网页版
    每周总结07
    每周总结06
    求循环数组中最大子数组的和
    每周总结05
    每周总结03
    每周总结04
    四则运算03
    开发过程记录
    Ubuntu下安装mysql和可视化工具超级简单教程
  • 原文地址:https://www.cnblogs.com/Hermioner/p/9860511.html
Copyright © 2011-2022 走看看