Java并发编程(二): 取消与中断

167 阅读5分钟

Java并发编程(二): 取消与中断

取消与中断的关系

Java中没有提供任何安全的机制来安全地终止线程,但是提供了中断,这是一种协作机制, 能够使一个线程终止另一个线程的当前工作。

一个可取消的任务必须拥有取消策略,这个策略中将详细地定义:其他代码如何取消该任务(How),任务在何时(When)检查是否已经请求了取消,以及在响应取消请求时应该执行哪些操作(What)。

通常情况下,中断是实现取消的最合理的方式。

中断

中断的原理

在Java的中断模型中,每一个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设置为true。在Thread中包含了中断线程以及查询线程中断状态的方法。

public class Thread {
    public void interrupt(){...}; // 中断目标线程
    public boolean isInterrupted(){...}; // 返回目标线程的中断状态
    public static boolean interrupted(){...}; //清除当前线程的中断状态
    ...
}

如何使用中断

什么是中断策略?

正如热舞中应该包含取消策略一样,线程同样应该包含中断策略。中断策略规定线程如何解释某个中断请求——当防线中断请求时,应该做哪些工作(如果需要的话),哪些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。

最合理的中断策略是某种形式的线程级(Thread-Level)取消操作或服务级(Service-Level)取消操作:**尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。**除此之外线程也可以包含其他中断策略,但是注意对于包含非标准中断策略的线程或线程池,只能用于能知道这些策略的任务中,也就是说,任务必须知晓它所运行的线程或线程池的中断策略。

谁使用中断?

线程应该只由他的所有者中断,所有者应该将线程的中断策略信息封闭在某个合适的取消机制之中,例如线程池的关闭(shutdown)方法。任务代码不应该对其执行所在的线程的中断策略作出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。

怎么响应中断?

通常情况下任务不会运行在自己拥有的线程之中,而是运行在某个服务(如线程池)所拥有的线程之中。对于非线程所有者的代码来说(例如,在线程池中,出来线程池实现以外的所有代码),应该小心地保存中断状态,这样拥有线程的代码才能够对中断做出响应,即使“非所有者”也可以对中断做出响应。

当调用可中断的阻塞函数时,可中断的阻塞函数会通过抛出InterruptedException来响应中断。有两种使用策略可以用于处理InterruptedException

  1. 传递异常(可能在执行某个特定于任务的清理操作之后),从而使你的方法也成为可中断的阻塞方法。
  2. 恢复中断状态,从而使调用栈中的上层代码也可以对其进行处理。

传递异常就是把InterruptedException添加到throws子句当中。

恢复中断状态用于无法传递InterruptedException的场景(例如通过Runnable来定义任务),并在合适的时候通过调用Interrupt来恢复线程的中断状态(这是一种标准的方法)。比较指定注意的是,对于不支持取消人可以调用可阻塞方法的操作,但是它们必须在循环中调用这些方法,并在发现中断后,将中断的状态先本地保存,然后重新尝试,最后在推出前恢复中断状态。如

public Task getNextTask(BlockingQueue<Task> queue){
    boolean interrupted = false;
    try{
        while(true){
            try{
                return queue.take();
            }catch(InterruptedException e){
                interrupted = true;
                //重新尝试
            }
        }
    }finally{
        if(interrupted) 
            Thread.currentThread().interrupt();
    }
}

在这个例子中,如果过早地恢复了中断状态,就是陷入无限循环。

如果代码中不会调用可中断的阻塞方法,可以通过轮询当前线程的中断状态来响应中断。

当调用不可中断的阻塞函数时我们可以根据线程具体的阻塞原因来响应中断。例如:**Java.io包中的同步SocketI/O。**在服务器应用程序中,最常见的阻塞I/O形式就是对套接字进行读取和写入。虽然InputStreamOutputStream中的readwrite等方法都不会响应中断,但是通过关闭底层的套接字,可以使得由于执行readwrite等方法而被阻塞的线程抛出一个SocketException。如:

public class ReaderThread extends Thread{
    private final Socket socket;
    private final InputStream in;
    
    public ReaderThread(Socket socket) throws IOException{
        this.socket = socket;
        this.in = socket.getInputStream();
    }
    
    public void interrupt(){
        try{
            socket.close();
        }catch(IOException ignored){}
        finally{
            super.interrupt();
        }
    }
    
    public void run(){
        try{
            byte[] buf = new byte[BUFSZ];
            while(true){
                int count = in.read(buf);
                if(count<0)
                    break;
                else if (count>0)
                    processBuffer(buf, count);
            }
        }catch(IOException e){
            // 允许线程退出
        }
    }
}

何时响应中断?

当检测到中断请求时,任务不需要放弃所有的操作来立刻响应中断,它可以推迟中断请求,并知道某个更合适的时刻。这项技术可以保证在更新过程中发生中断时数据结构不会被破坏。