Java并发编程实战笔记(第七章--取消与关闭)

要使任务和线程能安全地停下来,并不是一件容易的事。如果某个任务、线程、服务立即停止,会使共享的数据结构处于不一致的状态。当需要停止时,它们首先会清除当前正在执行的工作,然后结束。

1. 任务取消

如果外部代码能在某个操作正常完成之前将其置入"完成"状态,那么这个操作就称为可以取消的

取消某个操作的原因:

  • 用户请求取消
  • 有时间限制的操作:计时器超时
  • 应用程序事件
  • 错误:运行期错误
  • 关闭:当程序或者服务关闭时 

设置某个“已请求取消”标志,而任务定期查看这个标志,如果取消,那么任务将提前结束。

package chapter6;

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;

    public void run(){
        BigInteger p = BigInteger.ONE;
        System.out.println(Thread.currentThread().getName());
        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);
    }

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

        Thread thread = new Thread(primeGenerator);
        System.out.println(Thread.currentThread().getName());
        thread.start();
        Thread.sleep(3000);
        primeGenerator.cancel();

        System.out.println(primeGenerator.get());
    }
}

一个可取消的任务必须拥有取消策略:即其它代码如何(How)请求取消该任务,任务在何时(When)检查是否已经请求了取消,以及在响应取消请求时应该执行那些(what)操作。

中断

上面的代码有个问题:如果调用了一个阻塞方法(BlockingQueue.put),那么任务可能永远不会检查取消标志,因此永远不会结束。

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

public class BrokenPrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;
    private volatile boolean cancelled = false;

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

    @Override
    public void run() {
        try{
            BigInteger p = BigInteger.ONE;
            while(!cancelled){
                queue.put(p = p.nextProbablePrime());
            }
        } catch (InterruptedException e){

        }
    }

    public void cancel(){
        this.cancelled = true;
    }

    void consumePrimes() throws InterruptedException{
        BlockingQueue<BigInteger> primes =;
        BrokenPrimeProducer producer = new BrokenPrimeProducer(primes);
        producer.start();
        try{
            while(needMorePrimes())
                consume(primes.take());
        }finally {
            producer.cancel();
        }
    }
}

Thread中的中断方法:

public class Thread{
    /*中断目标线程*/
    public void interrupt(){...}

    /*f返回目标线程的中断状态*/
    public boolean isInterrupted(){...}

    /*清除当前线程的中断状态*/
    public static boolean interrupted(){...}
    ...
}


Thread.sleep()和Object.wait()等方法都会检查合适中断,并且发现中断时返回。
他们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException异常,
表示阻塞操作由于中断提前结束。
try{}catch(InterruptedException e){}

 中断策略

最合理的中断策略是某种形式的线程级取消操作或服务级取消操作: 尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。

区分任务和线程对中断的反应很重要。一个中断请求可以有一个或者多个接收者---中断线程池中的某个工作者线程,同时意味着“取消当前任务"和”关闭工作者线程“。大多数可阻塞的库函数都只是抛出InterruptedException作为中断响应。由于它们实现了最合理的取消策略:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。

由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则不应该中断这个线程。

响应中断

调用可中断的阻塞函数时(Thread.sleep,BlockingQueue.put等),有两种策略处理InterruptedException:

  1. 传递异常,从而使你的方法也成为可中断的方法。
  2. 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。
// 传递InterruptedException与将InterruptedException添加到throws子句一样简单
BlockingQueue<Task> queue;
...
public Task getNextTask() throws InterruptedException {
    return queue.take();
}

如果不想或无法传递InterruptedException,那么需要寻找另一种方式来保存中断请求。一种标准的方式就是通过再次调用interrupt来恢复中断状态【不能屏蔽InterruptedException】。只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和代码中都不应该屏蔽中断请求。

package chapter7;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * 〈一句话功能简述〉<br> 
 * 〈〉
 *
 * @author 我们
 * @create 2021/1/22
 * @since 1.0.0
 */
public class InterruptedExceptionDemo {
    public static Integer getNextTask(BlockingQueue<Integer> queue){
        boolean interrupted = false;
        try{
            while(true){
                try{
                    return queue.take();
                } catch (InterruptedException e){
                    interrupted = true;
                    // 重新尝试
                }
            }
        } finally {
            if (interrupted){
                Thread.currentThread().interrupt(); // 再次调用interrupt()恢复中断
            }
        }
    }
    public static void main(String[] args) throws InterruptedExcepiton{
        BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();

        queue.add(1);
        queue.add(2);
        queue.add(3);
        queue.add(4);

        System.out.println(getNextTask(queue));
        System.out.println(getNextTask(queue));
        System.out.println(getNextTask(queue));

    }
}

示例:计时运行

private static final SchedulExecutorService cancelExec = ...;

// 如果任务在超时之前完成,那么中断timedRun所在线程的取消任务将在timedRun返回到调用者之后启动,
// 如果任务不响应中断,那么timedRun会在任务结束时返回,此时已经超过了指定的时限
public static void timedRun (Runnable r, long timeout, TimeUnit unit){
    final Thread taskThread = Thread.currentThread();
    cancelExec.schedule(new Runnable(){
    @Override        
    public void run{
        // 在中断线程之前,应该了解它的中断策略。由于timeRun()可以被任意一个线程调用,因此无法
        // 知道这个调用线程的中断策略(处理InterruptedException的方法)
        taskThread.interrupt();
    },  timeout, unit);
    r.run();
}
           

 它依赖一个限时的join,因此存在join不足:无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回。

public static void timedRun(final Runnable r, long timeout, TimeUnit unit)
    throws InterruptedException{

    // 用RethrowableTask来捕获r执行过程中的异常
    class RethrowableTask implements Runnable{
        // 由于Throwable将在两个线程之间共享,所以声明为volatile,
        // 确保任务线程InterruptedException发布到timedRun中
        private volatile Throwable t; 
        public void run(){
            try{    r.run(); }
            catch (Throwable t) {    this.t = t; }
        }
        void rethrow(){
            if (t != null){
                throw launderThrowable(t);
            }
    }
    RethrowableTask task = new RethrowableTask();
    final Thread taskThread = new Thread(task);
    taskThread.start(); // 执行任务
    cancelExec.schedule(new Runnable(){
        public void run() {    taskThread.interrupt(); }
    }, timeout, unit);
    // 在当前线程中调用另外一个线程的join方法:则当前线程转为阻塞状态,等待调用join线程结束
    taskThread.join(unit.toMillis(timeout)); // taskThread等待Runnable任务结束
    task.rethrow(); // 抛出异常
}

通过Future来实现取消

我们已经使用了一种抽象的机制来管理任务的生命周期,处理异常,以及实现取消,即Future。

Future future = ExecutorService.submit();
boolean cancel(boolean mayInterruptIfRunning);
mayInterruptIfRunning:表示取消操作是否成功。
1)true且任务正在某个线程中运行,那么这个线程能够被中断。
2)false:若任务还有没有启动,就不要运行它。

什么情况下调用cancel可以将参数指定为true?
由标准的Executor创建的,它实现了一种中断策略使得任务可以通过中断被取消,所以如果任务在标准Executor中运行,并通过它们的Future来取消任务,那么可以设置true,当尝试取消某个任务时, 不宜直接中断线程池。
public static void timedRun(Runnable r, long timeout, TimeUnit unit)
    throws InterruptedException{
    Future<?> task = taskExec.submit(r); // 用线程池执行任务
    try{
        task.get(timeout, unit);  // 设置超时时间
    } catch (TimeoutException e){
        // 接下来任务将被取消
    } catch (ExecutionException e){
        // 如果在任务中抛出异常,那么重新抛出该异常
        throw launderThrowable(e.getCause());
    } finally {
        // !!!如果任务已经结束,那么执行取消操作也不会带来任何影响!!!
        task.cancel(true);     // 如果任务正在运行,那么将被中断
    }
}

当Future.get()抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel()来取消任务。

猜你喜欢

转载自blog.csdn.net/weixin_39443483/article/details/112910175