并发-取消与关闭


任务取消

  1. 取消

    • java中没有安全的将占方式来停止线程,只有一些协作式的机制
    • 其中一种协作机制,设置某个”已请求取消标志”,而任务将定期的查看该标志,如果设置了这个标志,任务将提前结束,(对于存在阻塞方法的任务,可能永远不会检查到取消标志,导致永远不会结束)
      while(!cancelled) //因为put为可阻塞方法,可能导致一直阻塞,导致无法检查取消标志
        blockQueue.put(p);
  2. 中断

    • 每个线程都有一个boolean类型的中断状态,当中断线程时,这个线程的中断状态将被设为true(仅设置状态)
      public class Thread{
        public void interrupt(){} //设置中断
        public boolean isInterrupted(){} //检查是否中断
        public static boolean interrupted(){} //清除当前线程的中断状态
      }
    • 阻塞库方法,如Thread.sleep和Object.wait等方法都会检查线程何时中断,并且在发现中断时提前返回,它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束,JVM不能保证阻塞方法检测到中断的速度,但实际情况中响应速度还是非常快的
    • 调用interrupt并不意味着立即停止目标线程正在执行的任务,而只是传递了请求中断的消息
  3. 中断策略

    • 最合理的中断策略是某种形式的线程级(Thread_Level)取消操作或服务级(Service-Level)取消操作;尽快退出,在必要时进行清理,通知某个所有者该线程已经退出
  4. 响应中断

    当调用可中断的阻塞函数时(中断后会抛出InterruptedException),有两种使用策略用于处理该异常

    • 传递(声明抛出)异常,从而使你的方法也成为可中断的阻塞方法
      public Task getNextTask() throws InterruptedException{ //传递异常
        return blockingQueue.take();
      }
    • 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理
  5. 通过Future来实现取消

    • Future作为一种抽象机制来管理任务的生命周期,处理异常,以及实现取消.通常使用现有库中的类比自行编写更好
    • boolean cancel(boolean mayInterruptRunning)方法参数为false时只能取消还没有开始的任务,对于已经开始的任务,就任由其运行下去;参数为true时,会中断正在执行的任务
      //通过Future取消任务
      public void timedRun(Runnable r,long timeout,TimeUnit unit) throws InterruptedException{
        //交给Future取消任务
        Future<?> task = taskExec.submit(r); //taskExec 为ExecutorService
        try {
            task.get(timeout,unit);
        } catch (ExecutionException e) {
            //可对异常进行处理,抛出详细异常
        } catch (TimeoutException e) {
            //取消任务
        } finally {
            //如果任务已经结束,cancel方法不会有任何影响;如果任务正在运行,将被中断
            task.cancel(true); 
        }
      }
  6. 处理不可中断的阻塞

    • 重写interrupt方法将非标准的取消操作封装在Thread中

      //重写interrupt方法将非标准的取消操作封装在Thread中
      class ReaderThread extends Thread{
        private final Socket socket;
      
        @Override
        public void interrupt() { //重写interrupt方法
            try {
                socket.close();
            } catch (IOException e) {
      
            } finally {
                super.interrupt();
            }
        }
      
        @Override
        public void run() {
            //任务
        }
      }
    • 将非标准的取消操作封装在一个任务(Future)中

      //扩展Callable接口
      interface CancellableTask<T> extends Callable<T>{
        void cancel();
        RunnableFuture<T> newTask();
      }
      //将非标准的取消操作封装到(Future)任务中
      abstract class SocketUsingTask<T> implements CancellableTask<T>{
        private Socket socket;
        //...
        @Override
        public synchronized void cancel() { //重写取消操作
            try {
                if(socket != null)
                    socket.close();
            } catch (IOException e) {}
        }
      
        @Override
        public RunnableFuture<T> newTask() { //将此Callable封装为任务返回
            return new FutureTask<T>(this){//封装为FutureTask
                //重写取消方法
                @Override
                public boolean cancel(boolean mayInterruptIfRunning) {
                    try {
                        SocketUsingTask.this.cancel(); //调用Callable重写的cancel方法
                    } finally {
                        return super.cancel(mayInterruptIfRunning);
                    }
                }
            };
        }
      }    
  7. 停止基于线程的服务

    • 服务应该提供生命周期方法(Lifecycle Method)来关闭它自己以及它所拥有的线程,这样服务关闭时,就可以关闭所有线程了,如ExecutorService中提供了shutdown和shutdownNow方法
  8. 关闭ExecutorService

    • shutdown正常关闭:等待队列中所有任务执行完成后才关闭
    • shutdownNow强制关闭:首先关闭当前正在执行的任务,然后返回所有尚未启动的任务清单(无法获知正在执行而被关闭的任务)
  9. 毒丸(Poison Pill)对象

    • 毒丸是指一个放在队列上的对象,当得到这个对象时,立即停止
    • 在FIFO队列中,毒丸对象将确保消费者在关闭之前首先完成队列中的所有工作,在提交毒丸对象之前提交的所有任务都会被处理,而之后的不会再处理

      处理非正常的线程终止

  10. 未捕获异常的处理

    • Thread中提供了UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况,当一个线程由于未捕获的异常退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器,如果没有提供,那默认将栈追踪信息输出到System.err
    • 可以实现该接口,主动的处理异常并写入日志中
    • Thread.setUncaughtExceptionHandler为线程设置异常处理器
    • Thread.setDefaultUncaughtExceptionHandler设置默认的异常处理器
    • ThreadPoolExecutor在构造函数中传入ThreadFactory,就会为线程池所有线程设置一个异常处理器
    • 在Executor中由submit提交的任务,不会交UncaughtExceptionHandler,而是被Future.get封装在ExecutionException中重新抛出

      JVM关闭

  11. 关闭钩子

    • 在JVM正常关闭中,JVM首先会调用所有已注册的关闭钩子(Shutdown Hook),关闭钩子并发执行,JVM并不保证关闭钩子的执行顺序,,当所有关闭钩子执行结束,如果runFinalizersOnExit为true,那么JVM将运行终结器,然后在停止,编写的关闭钩子必须要尽快退出,因为它们会延迟JVM的结束时间
    • Runtime.getRuntime().addShutdownHook(Threa thread); 注册关闭钩子
      //通过注册一个关闭钩子来停止日志服务
      public void start(){
        Runtime.getRuntime().addShutdownHook(new Thread(){
            public void run(){
                //停止日志服务
            }
        })
      }

      守护线程(Daemon Thread)

      当JVM中不存在任何一个正在运行的非守护线程时,则JVM进程即会退出,此时会丢弃掉所有现存的守护线程

当JVM停止时,所有仍然存在的守护线程都将被丢弃,既不执行finally块,也不会执行回卷栈,应该尽可能少的私用守护线程

创建一个守护线程,需要在调用thread.start()方法之前调用thread.setDeamon()方法.

终结器

GC回收器对那些定义了finalize方法的对象会进行特殊处理: 在回收器释放他们后,会调用他们的finalize方法,从而保证一些持久化的资源被释放

尽量避免使用终结器(当需要管理对象且该对象持有的资源是通过本地方法获得时才使用)


文章作者: Bryson
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Bryson !
评论
 上一篇
并发-任务执行 并发-任务执行
Executor框架 Executors工厂类 newFixedThreadPool创建一个可重用的固定线程数的线程池,以共享的无界队列方式来运行这些线程,当活动线程到达固定数量时,再次加入任务会加入队列等待其他活动线程运行结束,再执行
2019-08-24
下一篇 
并发-构建基础模块 并发-构建基础模块
同步容器类 Vector , Hashtable , Collections.synchronizedXxx等工厂方法创建 这些类实现线程安全方式: 将它们的状态封装起来,并对每个共有方法都进行同步,是的每次只有一个线程能方法容器状态
2019-08-20
  目录