任务&&线程的取消与关闭机制_中断

任务&&线程的取消与关闭机制_中断java

任务和线程的启动很容易。但要使任务和线程能安全、快速、可靠地中止下来,并非一件容易的事。Java没有提供任何机制来安全地终止线程。但它提供了中断(Interruption),这是一种协做机制,可以使一个线程终止另外一个线程的当前工做。安全

这种协做方式是很必要的,咱们不多但愿某个任务、线程或服务当即中止,由于这种当即中止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可使用一种协做的方式:当须要中止时,他们首先会清除当前正在执行的工做,而后在结束。这提供了更好的灵活性。由于任务自己的代码比发出取消请求的代码更清楚如何执行清楚操做。数据结构

 

使用“已请求取消”标志取消关闭任务和线程

看代码演示并发

package sync;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

/**
 */
public class PrimeGenerator implements Runnable {

    private final List<BigInteger> primes = new ArrayList<BigInteger>();


    private volatile boolean cancelled;//经过标志位取消线程任务的执行

    @Override
    public void run() {
        BigInteger p = BigInteger.ONE;
        while (!cancelled) {
            p = p.nextProbablePrime();
            synchronized (this) {
                primes.add(p);
            }
        }
    }

    public void cancel() {
        cancelled = true;
    }

    public synchronized List<BigInteger> get() {
        return new ArrayList<BigInteger>(primes); //注意这里要从新new一个对象,防止外部修改
    }


    public static void main(String args[]) throws InterruptedException {
        PrimeGenerator generator = new PrimeGenerator();
        new Thread(generator).start();
        try {
            Thread.sleep(1000);
        } finally {
            generator.cancel();
        }
        List<BigInteger> ps = generator.get();
        for (BigInteger b : ps) {
            System.out.println(b);
        }
        return;
    }
}

PrimeGenerator 采用了一种简单的取消策略:客户代码经过调用cancel来请求取消,PrimeGenerator 在每次搜索素数前首先检查是否存在取消请求,若是存在则退出。less

PrimeGenerator 中的取消机制最终会使搜索素数的任务退出,但在退出过程当中须要花费必定的时间。然而,若是使用了这种方法的任务调用了一个阻塞方法,例如BlockingQueue.put, 那么可能会产生一个更严重的问题——任务可能永远不会检查取消标志,所以永远不会结束。异步

 

经过中断取消关闭任务和线程

线程中断是一种协做机制,线程能够经过这种机制来通知另外一个线程,告诉他在合适的或者可能的状况下中止当前工做,并转而执行其余的工做。经过中断并不能直接终止另外一个线程,而须要被中断的线程本身处理中断。ide

这比如是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则彻底取决于本身。this

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

Java中断模型也是这么简单,每一个线程对象里都有一个boolean类型的标识(不必定就要是Thread类的字段,实际上也的确不是,这几个方法最终都是经过native方法来完成的),表明着是否有中断请求(该请求能够来自全部线程,包括被中断的线程自己)。例如,当线程t1想中断线程t2,只须要在线程t1中将线程t2对象的中断标识置为true,而后线程2能够选择在合适的时候处理该中断请求,甚至能够不理会该请求,就像这个线程没有被中断同样。线程

Thread中的中断方法:

public void interrupt():能中断目标线程(interrupt方法仅仅只是将中断状态置为true)

public boolean isInterrupted():返回目标线程的中断状态

public static boolean interrupted():将清除当前线程的中断状态,这也是清除中断状态的惟一方法

interrupt方法是惟一能将中断状态设置为true的方法。静态方法interrupted会将当前线程的中断状态清除。

/**
 * Tests whether the current thread has been interrupted.  The
 * <i>interrupted status</i> of the thread is cleared by this method.  In
 * other words, if this method were to be called twice in succession, the
 * second call would return false (unless the current thread were
 * interrupted again, after the first call had cleared its interrupted
 * status and before the second call had examined it).
 *
 * <p>A thread interruption ignored because a thread was not alive
 * at the time of the interrupt will be reflected by this method
 * returning false.
 *
 * @return  <code>true</code> if the current thread has been interrupted;
 *          <code>false</code> otherwise.
 * @see #isInterrupted()
 * @revised 6.0
 */
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}
/**
 * Tests whether this thread has been interrupted.  The <i>interrupted
 * status</i> of the thread is unaffected by this method.
 *
 * <p>A thread interruption ignored because a thread was not alive
 * at the time of the interrupt will be reflected by this method
 * returning false.
 *
 * @return  <code>true</code> if this thread has been interrupted;
 *          <code>false</code> otherwise.
 * @see     #interrupted()
 * @revised 6.0
 */
public boolean isInterrupted() {
    return isInterrupted(false);
}

阻塞库方法,例如Thread.sleep()和Object.wait()等,都会检查线程什么时候中断,而且在发现中断时提早返回。它们在响应中断时执行的操做包括:清除中断状态,抛出InterruptedException,表示阻塞操做因为中断而提早结束。‍JVM并不能保证阻塞方法检测到中断的速度,可是实际状况下响应的速度仍是很是快的。

 

当线程在非阻塞状态下中断时,他的中断状态将被设置,而后根据将被取消的操做来检查中断状态以判断发生了中断。经过这样的方法,中断操做将变得“有黏性”——若是不触发InterruptException,那么中断状态将一直保持,直到明确地清除中断状态。

调用interrupt并不意味着当即中止目标线程正在进行的工做,而只是传递了请求中断的消息。

一般,中断是实现取消的最合理方式。‍使用中断来取消任务和线程的示例

package sync;

import java.math.BigInteger;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 */
public class PrimeProducer extends Thread {

    private final BlockingQueue<BigInteger> queue;

    public PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!Thread.currentThread().isInterrupted()) {
                queue.put(p = p.nextProbablePrime());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }
    
    // 中断线程,将中断标志位设为 true
    public void cancel() {
        interrupt();
    }

    public synchronized BigInteger[] get() {
        return queue.toArray(new BigInteger[queue.size()]);
    }

    public static void main(String args[]) throws InterruptedException {
        BlockingQueue q = new ArrayBlockingQueue(500);
        PrimeProducer primeProducer = new PrimeProducer(q);
        primeProducer.start();
        try {
            Thread.sleep(2000);    //使当前线程等待2秒
        } finally {
            primeProducer.cancel();    //中断线程
        }

        if (primeProducer.isInterrupted()) {
            BigInteger[] bs = primeProducer.get();
            for (BigInteger b : bs) {
                System.out.println(b);
            }
        }
    }
}

在每次迭代循环中,有两个位置能够检测出中断:在阻塞的put方法调用中,以及在循环开始处查询中断状态时。因为调用了阻塞的put方法(线程内部在执行时能够检查中断状态这个值,来获知此线程是否应该结束了),所以这里并不必定须要进行显式的检测,但执行检测会使PrimeProducer对中断具备较高的响应性,由于他是在启动寻找素数任务以前检查中断的,而不是在任务完成以后。若是可中断的阻塞方法的调用频率并不高,不足以得到足够的响应性,那么显式的检测中断状态能起到必定的帮助做用。

 

可以设置中断状态的方法

除了 Thread.interrupt() 方法之外,下列 JDK 中的方法也会设置中断(也是经过调用 Thread.interrupt() 来实现的):

  • FutureTask.cancel()

  • ExecutorService.shutdownNow() 这个方法会调用线程池中全部线程的中断方法,不论它们是空闲的仍是运行中的

  • ExecutorService.shutdown() 方法只能中断空闲的线程

上面只是举两个 JDK 中应用到了线程中断的例子,这样的例子还有不少,就不一一列举了。固然,为了能响应中断,在你所写的 Runnable 或 Callable 代码中,必须经过 Thread.isInterrupted()、Thread.interrupted() 方法,或者捕获 InterruptedException 等的中断异常来发现线程中断并处理,不然线程是不会自行提早结束的。

 

中断的响应和处理

InterruptedException 是最多见的中断表现形式。因此如何处理 InterruptedException 便成为 Java 中断知识中的必修课。处理 InterruptedException 可有如下几种方式:

直接向上抛出

将异常不作任何处理,直接抛向该方法的调用者

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);
    }
 
    public Task getTask() throws InterruptedException { 
        return queue.take();
    }
}

 

在 catch 中作处理后在抛出

由于 InterruptedException 的抛出,会打断方法执行,使正在进行的工做只完成一部分。在有些状况下,你就须要进行诸如回滚的处理。因此在这种状况便须要在 catch 块中进行处理以后在向上抛出 InterruptedException。

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;
                 // Wait for two players to arrive and start a new game
                 playerOne = players.waitForPlayer(); // could throw IE
                 playerTwo = players.waitForPlayer(); // could throw IE
                 startNewGame(playerOne, playerTwo);
             }
         }
         catch (InterruptedException e) {  
             // If we got one player and were interrupted, put that player back
             if (playerOne != null)
                 players.addFirst(playerOne);
             // Then propagate the exception
             throw e;
         }
    }
}

 

不抛出InterruptedException时要恢复中断

不少时候,因为你所实现的接口定义的限制,你极可能没法抛出 InterruptedException。‍例如实现 Runnable 接口以编写业务代码。这时,你就没法再向上抛出 InterruptedException 了。‍此时你应该使用 Thread.currentThread().interrupt() 方法去恢复中断状态。由于阻塞方法在抛出 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 e) { 
             // Restore the interrupted status
             // 当抛出中断异常后,若是此时不能向上抛出,则须要恢复中断状态,不然中断状态会丢失
             Thread.currentThread().interrupt();
         }
    }
}

在这里,咱们要清楚中断的意义在于并发或异步场景下任务的终止。因此,若是你的代码在吞掉 InterruptedException 而不抛出时并不会形成任务没法被正确终止的状况方式,那也能够再也不恢复中断。

仍是 Runnable 的例子,你们都知道 Runnable 不少时候是用在线程池中,由线程池提供线程来运行。而且一般是做为任务的顶层容器来使用的,也就是说在线程池和 Runnable 实现之间,没有别的调用层了。那么在 try-catch InterruptedException 以后,即可不用在恢复线程中断了(固然,若是有回滚等的需求,仍是须要实现的)。由于经过异常的捕获,你已经能够正确终止线程了。

可是,若是不是上述状况,你所写的,捕获 InterruptedException 的方法会被其它的、非线程池类的方法调用。‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍例若有 A, B 两个方法,A 被 B 方法调用,A 中捕获 InterruptedException 后没有恢复线程中断,而 B 方法是一个无限循环,经过检查线程中断来退出,或者在调用 A 以后有个阻塞的方法。那便会形成线程没法按照指望被终止的状况发生。‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

 

本身抛出InterruptedException

有时候,你须要“无中生有”地创造出一个 InterruptedException 以表示中断的发生。固然,在这个时候,你也须要使用 Thread.isInterrupted() 或 Thread.interrupted() 来检测中断的发生。其实看了前面的部分咱们知道,抛出 InterruptedException 时,线程中断状态须要被清除()。这就是 Thread.interrupted() 的做用。‍其实,你要是看 JDK 源代码,就会发现,JDK 中并发类也是这么作的。

NOTE: 使用 Thread.interrupt() 和 InterruptedException 中的哪一种方法表示中断?

上面提到了一种状况是因为接口的限制而没法抛出 InterruptedException,这时你别无选择,只能用 Thread.interrupt() 恢复中断。除了这种状况,其它的时候推荐使用 InterruptedException 来表示中断。当方法声明抛出 InterruptedException 时,它就是在告诉调用者,我这个方法可能会花费不少的时间,而你能够经过线程中断来终止调用。经过 InterruptedException 来表示中断,含义更清晰,反应也更迅速。

===========END===========

相关文章
相关标签/搜索