取消与关闭

任务取消(cancel)
若外部代码能在某个操作正常完成之前将其置入“完成”状态,则该操作是 可取消的
取消操作的原因:

       1. 用户请求取消。

       2. 有时间限制的操作,如超时设定

       3. 应用程序事件。

       4. 错误。

       5. 关闭。 

如下面这种取消操作实现:
/**
 * 一个可取消的素数生成器
 * 使用volatile类型的域保存取消状态
 * 通过循环来检测任务是否取消
 */
@ThreadSafe
public class PrimeGenerator implementsRunnable {
    private final List<BigInteger> primes = new ArrayList<>();
    private volatile boolean canceled;
    @Override
    public void run() {
        BigInteger p = BigInteger.ONE;
        while(!canceled){
            p = p.nextProbablePrime();
            synchronized(this) { //同步添加素数
                primes.add(p);
            }
        }
    }
    /**
     * 取消生成素数
     */
    publicvoidcancel(){
        canceled = true;
    }
    /**
     * 同步获取素数
     * @return 已经生成的素数
     */
    public synchronized List<BigInteger> get(){
        return new ArrayList<>(primes);//避免primes逸出
    }
}

public class PrimeGeneratorTest {
    public static void main(String[] args) {
        PrimeGenerator pg = new PrimeGenerator();
        new Thread(pg).start();
        try{
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        } finally{
            pg.cancel(); //取消
        }
        System.out.println("all primes: "+ pg.get());
    }
}

上面例子中,取消操作虽然最终会导致任务结束,但并不是立刻结束,需要花费一些时间,如果任务在某个操作上阻塞,如从BlockingQueue中取数据,则该任务可能永远不检查取消标志,因此永远不会终结。这时可以使用中断。

中断
调用interrupt()不会立刻中断正在执行的线程,只是将线程的中断标识位设置成true。 线程会在下 一个 取消点 中断自己,也就是说, 线程在调用sleep()、join()、wait()方法时会收到线程中断信号,则会抛出InterruptedException,在catch块中捕获到这个异常时, 线程的中断标志位已经被设置成false了 ,因此在catch或finally块中调用isInterrupted()、Thread.interrupted()始终都为false。 如果线程没有被阻塞,则interrupt()将不起作用, 其实在sleep、wait、join这些方法内部会不断检查中断状态的值(通过Thread.interrupted(),所以标识会被重置为false),发现中断后自己抛出InterruptedException。
finally块怎么知道是否被中断过呢?可以在catch块中恢复中断,也就是再一次调用interrupt方法。

通常,中断是实现取消的合理方式

下面通过中断实现取消功能:

/**
 * 通过中断来实现取消
 * 不采用boolean变量,
 * 防止在queue.put()时由于阻塞,不能检查到boolean变量而无法取消
 * 但使用interrupt就可以,
 * 即使queue.put()阻塞, 也会检查到interrupt信号,从而抛出IntteruptedException
 * 从而达到取消的目的
 */
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()){//说明1
                queue.put(p = p.nextProbablePrime());//说明2
            }
        } catch(InterruptedException e) {
            // thread exit
        }
    }
    /**
     * 取消
     */
    public void cancel(){
        interrupt(); //中断当前线程
    }
}

说明1:

为什么还要判断线程是否被中断呢?因为如果put操作不阻塞的话,即使线程被中断了,也会由于还没有执行到取消点而没有抛出异常,所以线程会一直执行。

说明2:

put操作如果发生阻塞,此时如果调用interrupt中断线程,则会抛出异常,因为阻塞操作包含了取消点,捕获到异常后,可以做一些结束前的工作。

注意:使用中断时,如果有循环,通常都要在循环中判断是否被中断(isInterrupted())。

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

1. 传递异常。

2. 恢复(保存)中断状态,从而使调用栈的上层代码能够对其进行处理:

如果你不想或者不能传递InterruptedException,则需要用另一种方式保存中断请求,实现这个目的的标准方法是再次调用 interrupt()来恢复中断状态,因为一个中断请求一旦被捕获到之后,也就是说抛出InterruptedException异常之后,则该信号就消失了(线程的中断标识也被清零),所以,如果捕获到中断请求而不想处理也不想直接向外抛出,则需要再一次调用interrupt(),此时上层代码便也会收到一个中断请求。

interrupted 和 isInterrupted方法的区别:

注意,interrupted不是interrupt。

调用interrupt()中断一个线程时,会把该线程的中断标识设为true,调用该线程对象的isInterrupted()方法可以查询该中断标识,只查询不改变,而调用静态方法interrupted()来查询中断标识时,除了返回中断标识,还会把中断标识设为false。


通过Future实现取消

public void timedRun(Runnable r, longtimeout, TimeUnit unit)
            throws InterruptedException {
    Future<?> task = taskExec.submit(r);
    try{
        task.get(timeout, unit);
    } catch(ExecutionException e) {
        //任务执行中抛出异常
    } catch(TimeoutException e) {
        //任务超时处理
    } finally{
        //如果任务执行完毕,则没有影响; 如果任务执行中,则会中断任务
        if(task != null) task.cancel(true);
    }
}

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

处理不可中断的阻塞
造成线程阻塞的原因:

1、java.io包中的同步Socket I/O。如套接字中进行读写操作readwrite方法,不会响应中断,但是可以通过关闭底层的Socket,让read或write抛出一个SocketException,所以可以用socket.close()来处理阻塞。

2. java.nio包中的同步I/O如当中断或关闭正在InterruptibleChannel上等待的线程时,会对应抛出ClosedByInterruptExceptionAsynchronousCloseException

3. Selector的异步I/O。如果一个线程在调用Selector.select时阻塞了,则不会响应中断(interrupt),但是调用close或 wakeup会使线程抛出ClosedSelectorException

4、等待获得内部锁时。如Synchronized阻塞时不会响应中断。但Lock类的lockInterruptibly允许在等待锁时响应中断。

/**
 * 通过改写interrupt方法将非标准的取消操作封装在Thread中
 */
publicclassReaderThread extendsThread {
    privatefinalSocket socket;
    privatefinalInputStream in;
    privateintbufferSize;
    publicReaderThread(Socket socket, InputStream in) {
        this(socket, in, 1024);
    }
    publicReaderThread(Socket socket, InputStream in, intbufferSize) {
        this.socket = socket;
        this.in = in;
        this.bufferSize = bufferSize;
    }

    @Override

    public void interrupt() {
        try{
            socket.close(); //中断前关闭socket
        } catch(IOException e) {
        } finally{
            super.interrupt();
        }
    }
    @Override
    publicvoidrun() {
        try{
            byte[] buf = newbyte[bufferSize];
            while(true) {
                intcount = in.read(buf);
                if(count < 0) {
                    break;
                } elseif(count > 0) {
                    processBuffer(buf, count);
                }
            }
        } catch(IOException e) {
            // 线程中断处理
        }
    }
       ...
}

采用newTaskFor来封装非标准的取消
/**
 * 可取消的任务接口
 */
publicinterfaceCancellableTask<T> extendsCallable<T> {
    voidcancel();
    RunnableFuture<T> newTask();
}
/**
 * 使用了Socket的任务
 * 在取消时需要关闭Socket
 */
publicabstractclassSocketUsingTask<T> implementsCancellableTask<T> {
    privateSocket socket;
    publicvoidsetSocket(Socket socket) {
        this.socket = socket;
    }
    @Override
    publicT call() throwsException {
        //do working
           ...
    }
    @Override
    publicsynchronizedvoidcancel() {
        try{
            if(socket != null){
                socket.close();
            }
        } catch(IOException ignored) {
        }
    }
    @Override
    publicRunnableFuture<T> newTask() {
        returnnewFutureTask<T>(this){
            @Override
            publicbooleancancel(booleanmayInterruptIfRunning) {
                try{
                    SocketUsingTask.this.cancel();
                } catch(Exception ignored) {
                }
                returnsuper.cancel(mayInterruptIfRunning);
            }
        };
    }
}
/**
 * 通过newTaskFor将非标准的取消操作封装在任务中
 */
publicclassCancellingExecutor extendsThreadPoolExecutor {
    publicCancellingExecutor(intcorePoolSize, intmaximumPoolSize,
            longkeepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
    @Override
    protected<T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        if(callable instanceofCancellableTask){ //若是我们定制的可取消任务
            return((CancellableTask<T>)callable).newTask();
        }
        returnsuper.newTaskFor(callable);
    }
}

停止基于线程的服务
正确的封装原则:
除非 拥有某个线程 ,否则不能对该线程 进行操控 。如 中断线程 修改线程优先级 等。
应用程序通常会创建持有线程的服务,只要这些 服务的存在时间 大于 创建这些服务的方法的存在时间 ,那么就应该提供 生命周期的方法 如果应用程序优雅地退出,则这些服务的线程也需要结束。
和其它被封装的对象一样,线程的所有权是不可传递的(最好不要传递,传递则破坏了封装性),比如,应用程序拥有服务,服务拥有工作线程,但是,应用程序并不拥有工作线程,所以,应用程序不应该试图直接停止工作线程,而是服务应该提供生命周期方法来关闭它自己,并关闭它所拥有的线程,那么当应用程序关闭这个服务时,服务就可以关闭其所有的线程了,而不用应用程序自己来管理这些线程(这样做很容易漏掉)。比如, ExecutorService提供的 shutdown()、 shutdownNow()方法来关闭线程池。
ExecutorService提供两种关闭服务的方法:

1、 shutdown: 安全关闭。不再接受新任务提交,待所有队列中的任务执行完成再关闭。这些任务什么时候执行完呢?可以调用 awaitTermination(timeout, unit),该方法会阻塞直到所有任务执行完为止,或者超时,或者当前线程被中断。通常这两个方法结合使用,关闭之后,等待关闭完成。

2、shutdownNow强行关闭。不再接受新任务提交,停止正在执行的任务,并返回未开始执行的任务列表,注意:

返回的任务可能并不是提交给ExecutorService的相同对象,它们可能是经过包装的已提交任务的实例,所以,不能简单地用"=="来判断返回的对象是否是提交之前的对象。

/**
 * 封装ExecutorService实现日志服务
 */
publicclassLogService2 {
    privatefinalExecutorService exec = Executors.newSingleThreadExecutor();
    privatefinalPrintWriter writer;
    publicLogService2(PrintWriter writer){
        this.writer = writer;
    }
    /**
     * 产生日志
     * @param msg 日志内容
     * @throws InterruptedException
     */
    publicvoidlog(String msg) throwsInterruptedException{
        exec.execute(newWriteTask(msg));
    }
    /**
     * 停止日志服务
     * @throws InterruptedException
     */
    publicvoidstop(longtimeout, TimeUnit unit) throwsInterruptedException{
        try{
            exec.shutdown(); //平缓关闭服务
            //关闭服务后, 阻塞到所有任务被执行完毕或者超时发生,或当前线程被中断
            exec.awaitTermination(timeout, unit);
        } finally{
            writer.close();
        }
    }
    ...
}
“毒丸”对象
毒丸:指一个放在队列上的对象,当得到这个对象时,就立即停止
shutdownNow的局限性:
当通过shutdownNow强行关闭一个 ExecutorService时 ,它试图取消正在进行的任务,并返回那些已经提交但还没开始的任务(可能是经过包装的已提交任务的实例),然而。我们无法通过常规方法来得知哪些任务已经开始但未结束。下面例子,通过封装 ExecutorService,并重写execute(类似的,还可以重写submit),通过向该封装类提交任务来记录那些任务在关闭后被取消,这里记录的是开始了但还没结束的任务,已经结束的任务和还没开始的任务不会被加到集合中。如果外面直接向成员对象ExecutorService exec提交任务,则该任务被中断的时候不会像下面代码一样在finally中判断是否被中断,从而不能加到被中断任务的集合中。
/**
 * 在ExecutorService中跟踪在关闭之后被取消的任务
 */
publicclassTrackingExecutor extendsAbstractExecutorService {
    privatefinalExecutorService exec;
    privatefinalSet<Runnable> tasksCancelledAtShutdown =
            Collections.synchronizedSet(newHashSet<Runnable>());
     
    publicTrackingExecutor(ExecutorService exec) {
        this.exec = exec;
    }
    /**
     * 获取关闭后取消的任务
     */
    publicList<Runnable> getCancelledTasks(){
        if(!exec.isTerminated()){
            thrownewIllegalStateException("service doesn't stop");
        }
        returnnewArrayList<>(tasksCancelledAtShutdown);
    }
    @Override
    publicvoidexecute(finalRunnable command) {
        exec.execute(newRunnable() {
            @Override
            publicvoidrun() {
                try{
                    command.run();
                } finally{
                    if(isShutdown() && //若Executor已经关闭了
                            Thread.currentThread().isInterrupted()){ //应该在finally前面加一个catch,然后恢复中断,否则这里中断标识应该会总是false
                        tasksCancelledAtShutdown.add(command);
                    }
                }
            }
        });
    }
}
处理非正常的线程终止
当一个线程由于未捕获异常而退出时,jvm会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器(线程API提供了该工具)。若没有提供任何异常处理器,则默认行为是将栈追踪信息输出到System.err
可以通过下面两种方式设置线程的异常处理器,当然还有其它方式:
1、thread.setUncaughtExceptionHandler(xxx); //设置某一个线程的异常处理器
2、Thread.setDefaultUncaughtExceptionHandler(xxx);//设置所有线程的默认异常处理器
JVM首先寻找线程自己的异常处理器,如果没有则再寻找默认的处理器,两者只能有一个被调用。
定义一个异常处理器如下:
/**
 * 将异常写入日志的UncaughtExceptionHandler
 */
publicclassUEHLogger implementsUncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.SEVERE, "the thread with exceptoin: "+t.getName(), e);
    }
}
在运行时间较长的应用程序中,通常会为所有线程的未捕获异常制定同一个异常处理器,并且该处理器至少会将异常信息记录到日志中。
对于线程池而言,可以用Runnable或Callable把任务包装起来,通过包装加入异常捕获,或者重写ThreadPoolExecutor的afterExecute钩子方法。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值