【Java多线程】Java中断机制

目录

1 前提

2 何为中断

3 中断的好处

4 Thread类的中断相关方法

5 如何处理InterruptedException

5.1 不捕捉 InterruptedException,将它传播给调用者

5.2 捕捉InterruptedException,然后重新抛出

5.3 捕捉 InterruptedException 后恢复中断状态

5.4 生吞中断 —— 一般不要这么做

6 实现可取消的任务

7 实现不可取消的任务


1 前提

假设程序需要停止正在运行的线程,如果直接stop线程,则有可能导致程序运行不完整、造成数据的不一致性以及其它不可控情况,因此Java提供了中断机制。

2 何为中断

中断是一种协作机制。当一个线程中断另一个线程时,被中断的线程不一定要立即停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方便的时候停止它正在做的事情。有些方法,例如 Thread.sleep(),很认真地对待这样的请求,但每个方法不是一定要对中断作出响应。对于中断请求,不阻塞但是仍然要花较长时间执行的方法可以轮询中断状态,并在被中断的时候提前返回。 您可以随意忽略中断请求,但是这样做的话会影响响应。对中断请求的处理见下面内容。

每个线程都有一个与之相关联的 Boolean 属性,用于表示线程的中断状态(interrupted status)。中断状态初始时为 false。当另一个线程通过调用 Thread.interrupt() 中断一个线程时,会出现以下两种情况。在被中断线程中运行的代码以后可以轮询中断状态,看看它是否被请求停止正在做的事情。

  1. 若被中断的线程正在执行一个低级可中断阻塞方法( Thread.sleep()、Thread.join() 或 Object.wait(),LockSupport.part()不行)时,那么它将取消阻塞并抛出 InterruptedException(并不会使被中断线程停止执行),抛出异常后将中断标志置为false。
  2. 若被中断线程没有在执行可中断阻塞方法,则只是将中断标志置为true,等待该线程后面的处理。
  3. 若线程阻塞在一个I/O操作如java.nio.channels.InterruptibleChannel实现类的某些方法,则会将线程的中断标志置为true,同时抛出java.nio.channels.ClosedByInterruptException,最后关闭通道。
  4. 若线程阻塞在java.nio.channels.Selector,则会将线程的中断标志置为true,然后立即从selection操作返回。

      注意:若被中断线程执行完,不管之前是否被中断过,都会将中断标志置为false。

wait()和join()被中断的过程参考:https://blog.csdn.net/qq_34039868/article/details/105103430

我们可以写一点代码来测试一下上面1,2的两种情况,下面是一个Test类继承了Thread,给出了main方法,重写的run方法见下面内容。

public class Test extends Thread {

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();

        test.start();
        // 为了给test线程留出充足时间执行
        Thread.sleep(1000);

        System.out.println("中断前的test线程的中断标志:"+test.isInterrupted());

        test.interrupt();                           // 线程中断

        // 为了给test.interrupt()留出充足的时间执行
        Thread.sleep(1000);

        System.out.println("中断后的test线程的中断标志:"+test.isInterrupted());

    }
}

下面的run方法中,在while循环中不断的执行sleep方法,保证在对线程进行中断(test.interrupt())的时候它依然处在睡眠状态。可以看到线程中断标志是false,中断后抛出了InterruptedException,然后再获取中断标志还是false,说明抛出InterruptedException后确实将中断标志置重置了。同时线程任然还在执行。

    // run方法1
    @Override
    public void run(){
        while(true){
            try {
                System.out.println("test线程执行sleep方法");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("睡眠结束,test线程还在运行...");
        }
    }

/**--------------------执行结果----------------------
 * test线程执行sleep方法
 * 中断前的test线程的中断标志:false
 * java.lang.InterruptedException: sleep interrupted
 * at java.lang.Thread.sleep(Native Method)
 * at com.hust310.aircraft.Test.run(Test.java:16)
 * 睡眠结束,test线程还在运行...
 * test线程执行sleep方法
 * 中断后的test线程的中断标志:false
 * 睡眠结束,test线程还在运行...
 * test线程执行sleep方法
 * ......
*/

下面的run方法没有sleep、wait和join方法,只是一个死循环,被中断后不会抛出InterruptedException,只是将中断标志置为true。

    // run方法2
    @Override
    public void run(){
        while (true){}
    }

/**
 * 中断前的test线程的中断标志:false
 * 中断后的test线程的中断标志:true
 * ......
*/

下面的run方法没有运行任何代码,可以看到中断后的线程中断标志仍然为false,说明线程结束后自动将中断标志重置了。

    // run方法3
    @Override
    public void run(){

    }

/**
 * 中断前的test线程的中断标志:false
 * 中断后的test线程的中断标志:false
 * Process finished with exit code 0
*/

3 中断的好处

中断的协作特性所带来的一个好处是,它为安全地构造可取消活动提供更大的灵活性。我们很少希望一个活动立即停止;如果活动在正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态。中断允许一个可取消活动来清理正在进行的工作,恢复不变量,通知其他活动它要被取消,然后才终止。

4 Thread类的中断相关方法

每个Java线程都有一个Boolean类型的属性用于表示中断标识。Java线程提供了3个方法用于操作此中断标识:

public static boolean interrupted()

测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

public boolean isInterrupted() 测试线程是否已经中断。线程的中断状态不受该方法的影响
public void interrupt()中断线程。

5 如何处理InterruptedException

如果抛出 InterruptedException 意味着一个方法是阻塞方法,那么调用一个阻塞方法则意味着您的方法也是一个阻塞方法,而且您应该有某种策略来处理 InterruptedException

5.1 不捕捉 InterruptedException,将它传播给调用者

通常最容易的策略是自己抛出 InterruptedException,如下面 putTask() 和 getTask() 方法中的代码所示。 这样做可以使方法对中断作出响应,并且只需将 InterruptedException 添加到 throws 子句,这样一来 putTask() 和 getTask()的上层调用者就能及时感知到中断动作并进行处理。

public class TaskQueue {
    private static final int MAX_TASKS = 1000;
 
    private BlockingQueue<Task> queue 
        = new LinkedBlockingQueue<Task>(MAX_TASKS);
 
    public void putTask(Task r) throws InterruptedException { 
        queue.put(r);    // put()方法上throws InterruptedException 
    }
 
    public Task getTask() throws InterruptedException { 
        return queue.take();    // take()方法上throws InterruptedException 
    }
}

5.2 捕捉InterruptedException,然后重新抛出

有时候需要在传播异常之前进行一些清理工作。在这种情况下,可以捕捉 InterruptedException,执行清理,然后抛出异常。下面代码演示了这种技术,该代码是用于匹配在线游戏服务中的玩家的一种机制。 matchPlayers() 方法等待两个玩家到来,然后开始一个新游戏。如果玩家1已到来,但是玩家2仍未到来之际该方法被中断,那么它会将玩家1放回队列中,然后重新抛出 InterruptedException,这样玩家1对游戏的请求就不至于丢失。

public class PlayerMatcher {
    private PlayerSource players;
 
    public PlayerMatcher(PlayerSource players) { 
        this.players = players; 
    }
 
    public void matchPlayers() throws InterruptedException { 
        try {
             Player playerOne, playerTwo;
             while (true) {
                 playerOne = playerTwo = null;
                 // 等待匹配到两个玩家然后开始游戏
                 playerOne = players.waitForPlayer(); // 这里可能抛出InterruptedException 
                 playerTwo = players.waitForPlayer(); // 这里可能抛出InterruptedException
                 startNewGame(playerOne, playerTwo);
             }
         }
         catch (InterruptedException e) {  
             // 若palyerTwo的匹配中断,则将playerOne返回到匹配队列中
             if (playerOne != null)
                 players.addFirst(playerOne);
             // 重新抛出InterruptedException 给上层调用者
             throw e;
         }
    }
}

5.3 捕捉 InterruptedException 后恢复中断状态

5.3相对于5.2最大的区别就是:有时候在catch到InterruptedException 后不能继续抛给上一层调用者,但我们有想要让上层调用者感知到中断动作。比如:你想在被中断线程的run方法中直接catch异常,但是我们重写的run方法并没有采用向上层抛出异常的机制,这时你就必须要采用其它方法来通知上层调用者了。

当一个阻塞方法检测到中断并抛出 InterruptedException 时,它清除中断状态,将中断标志设为false。如果捕捉到 InterruptedException 但是不能重新抛出它,那么应该保留中断发生的证据,以便调用栈中更高层的代码能知道中断,并对中断作出响应。该任务可以通过调用 interrupt() 以 “重新中断” 当前线程来完成,如下面代码所示所示。至少,每当捕捉到 InterruptedException 并且不重新抛出它时,就在返回之前重新中断当前线程。

public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
 
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
 
    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException e) { 
             // 将线程的中断标志设为true
             Thread.currentThread().interrupt();
         }
    }
}

5.4 生吞中断 —— 一般不要这么做

处理 InterruptedException 时采取的最糟糕的做法是生吞它 —— 捕捉它,然后既不重新抛出它,也不重新断言线程的中断状态。对于不知如何处理的异常,最标准的处理方法是捕捉它,然后记录下它,但是这种方法仍然无异于生吞中断,因为调用栈中更高层的代码还是无法获得关于该异常的信息。(仅仅记录 InterruptedException 也不是明智的做法,因为等到人来读取日志的时候,再来对它作出处理就为时已晚了。) 下面代码展示了一种使用得很广泛的模式,这也是生吞中断的一种模式:

// 不要这么做
public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
 
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
 
    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException swallowed) { 
             // 记得要在这里恢复中断状态
         }
    }
}

如果不能重新抛出 InterruptedException,不管您是否计划处理中断请求,仍然需要重新中断当前线程,因为一个中断请求可能有多个 “接收者”。在使用线程池处理任务时,每当用户发起请求,就会产生一个任务。此时线程池一般会创建一个线程来处理这个任务。标准线程池 (ThreadPoolExecutor)worker 线程实现负责中断,因此中断一个运行在线程池中的任务可以起到双重效果,一是取消任务,二是通知线程池回收执行该任务的线程以便执行后面的任务。如果任务生吞中断请求,则 worker 线程将不知道有一个被请求的中断,从而耽误应用程序或服务的关闭。

6 实现可取消的任务

仅仅因为一个任务是可取消的,并不意味着需要立即 对中断请求作出响应。对于执行一个循环中的代码的任务,通常只需为每一个循环迭代检查一次中断。取决于循环执行的时间有多长,任何代码可能要花一些时间才能注意到线程已经被中断(或者是通过调用 Thread.isInterrupted() 方法轮询中断状态,或者是调用一个阻塞方法)。 如果任务需要提高响应能力,那么它可以更频繁地轮询中断状态。阻塞方法通常在入口就立即轮询中断状态,并且,如果它被设置来改善响应能力,那么还会抛出 InterruptedException

当程序被设计成在被中断时退出,你可以生吞中断,不用对InterruptedException 进行处理。下面代码演示了这种情况,该线程列举素数,直到被中断时停止,这里还允许该线程在被中断时退出。循环在两个地方检查是否有中断:一处是在 while 循环的头部轮询 isInterrupted() 方法,另一处是调用阻塞方法 BlockingQueue.put()

public class PrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;
 
    PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }
 
    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!Thread.currentThread().isInterrupted())
                queue.put(p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {
            // 在这里可以使线程退出
        }
    }
 
    public void cancel() { interrupt(); }
}

7 实现不可取消的任务

有些任务拒绝被中断,这使得它们是不可取消的。但是,即使是不可取消的任务也应该尝试保留中断状态,以防在不可取消的任务结束之后,调用栈上更高层的代码需要对中断进行处理。下面代码展示了一个方法,该方法等待一个阻塞队列,直到队列中出现一个可用项目,而不管它是否被中断。为了方便他人,它在结束后在一个 finally 块中恢复中断状态,以免剥夺中断请求的调用者的权利。(它不能在更早的时候恢复中断状态,因为那将导致无限循环 —— BlockingQueue.take() 将在入口处立即轮询中断状态,并且,如果发现中断状态集,就会抛出 InterruptedException。)

public Task getNextTask(BlockingQueue<Task> queue) {
    boolean interrupted = false;
    try {
        while (true) {
            try {
                return queue.take();
            } catch (InterruptedException e) {
                interrupted = true;
                // fall through and retry
            }
        }
    } finally {
        if (interrupted)
            Thread.currentThread().interrupt();
    }
}

参考文章:https://blog.csdn.net/demon7552003/article/details/89020491

                  https://www.ibm.com/developerworks/cn/java/j-jtp05236.html#icomments

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值