并发-线程池的使用


线程饥饿死锁

  • 在线程池中,如果任务依赖于其他任务,那么可能产生死锁

  • 饥饿: 一个线程在无限的等待另外一个或多个线程相互传递使用并且永不会释放的资源

  • 死锁: 可以认为是两个线程或者进程在请求对方占有的资源

    运行时间较长的任务

  • 如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕

  • 其中一种解决方案是限定任务等待资源的时间,而不要无限制的等待

  • 类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本,如Thread.join丶BolockingQueue.put丶CountDownLatch.await和Selector.select等,如果等待超时,那么可以把任务标识为失败,然后终止任务或者重新放回队列以便后续执行

    设置线程池的大小

  • 要正确设置线程池的大小,必须分析计算环境丶资源预算和任务特性.部署的系统中有多少个CPU?多大的内存?任务是计算(CPU)密集型丶I/O密集型还是二者皆可?它们是否需要像JDBC连接这样的稀缺资源

  • 对于计算(CPU)密集型任务,线程池的大小一般设置为N(cpu) + 1,通常能实现最优利用率

    配置ThreadPoolExecutor

  • ThreadPoolExecutor为一些Executor提供了基本的实现,如newCachedThreadPool丶newFixedThreadPool丶newScheduledThreadExecutor等工厂方法返回的

  • 如果默认的执行策略不能满足需求,那么可以使用ThreadPoolExecutor的构造函数来实例化一个对象,根据自己需求定制,可以参考Executors工厂类的源码

      public ThreadPoolExecutor(
          int corePoolSize,   //基本大小
          int maximumPoolSize,    //最大大小
          long keepAliveTime, //存活时间(空闲时间超过存活时间时,将被标记为可回收且线程池的当前大小超过基本大小时,该线程将被终止)
          TimeUnit unit, //存活时间单位
          BlockingQueue<Runnable> workQueue,//等待执行的任务放入的队列(无界,有界,同步移交)
          ThreadFactory threadFactory, //每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的
          RejectedExecutionHandler handler //饱和策略,有界队列被填满后,拒绝新任务的策略
      ){ ... }
  • 饱和策略:

    • setRejectedExecutionHandler设置ThreadPoolExecutor的饱和策略
    • AbortPolicy 终止策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException,调用者可捕获异常然后处理
    • CallerRunsPolicy 调用者运行是一种调节机制,该机制会将某些任务回退到调用者,由调用者线程执行,这样会制约调用者线程一段时间内不能提交任务(回退任务没有执行完成前),在这期间,调用者线程不会调用accept,因此到达的请求将被保存在TCP层的队列中而不是在应用程序的队列中,如果持续过载,TCP层将最终发现它的请求队列被填满,因此同样会开始抛弃请求
    • DiscardPolicy 抛弃策略会抛弃新提交的任务
    • DiscardOldestPolicy 抛弃最旧的策略会抛弃下一个将被执行的任务,然后尝试重新提交新的任务
  • 线程工厂

    • 每当线程池需要创建一个线程时,都是通过线程工厂(ThreadFactory)方法来完成的,如果要利用安全策略来控制对某些特殊代码库的访问权限,可通过Executor中的privilegedThreadFactory工厂来定制自己的线程工厂,这将与创建privilegedThreadFactory的线程拥有相同的访问权限,新线程的 accesscontrolcontext 和 contextclassloader 的其他设置与调用此 privilegedthreadfactory 方法的线程相同,如果不使用该方式,线程将从调用execute或者submit的客户程序中继承访问权限
  • 通过构造函数创建ThreadPoolExecutor对象后再设置

    • ThreadpoolExecutor对象提供的setter方法来设置参数(线程池基本大小,最大大小,存活时间,线程工厂,拒绝执行处理器)
    • 如果是通过Executors中的工厂方法创建的(newSingleThreadExecutor除外),那么可以将结果的类型强转为ThreadPoolExecutor进行设置
    • Executors中有一个unconfigurableExecutorService工厂方法,该方法对现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对其配置,newSingleThreadExecutor返回按这种方式封装的ExecutorService
  • 扩展ThreadPoolExecutor

    • ThreadPoolExecutor中提供了beforeExecute丶afterExecute丶terminated方法可以在其子类中进行改写,从而扩展其行为,可以添加日志丶计时丶监视丶统计信息收集
    • beforeExecute如果抛出异常,那么任务将不会被执行,afterExecute也不会被调用
    • afterExecute: 无论任务是从run中正常返回,还是抛出异常返回,afterExecute都会被调用,如果任务完成后带有一个Error,那么就不会调用afterExecute
    • terminated: 线程池完成关闭操作时调用,也就是所有任务都已完成并且所有工作者线程都已关闭后,terminated可以用来释放Executor在其生命周期里分配的各种资源,此外可执行发送通知,记录日志或收集finalize统计信息等操作

文章作者: Bryson
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Bryson !
评论
 上一篇
并发-避免活跃性问题 并发-避免活跃性问题
死锁 当一个线程永远的持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞 eg: 1.线程A持有锁L并想获得锁M,同时线程B持有锁M并尝试获取锁L,那么这两个线程将永远地等待下去,这种情况就是最简单的死锁形式,称为抱死(D
2019-08-27
下一篇 
并发-任务执行 并发-任务执行
Executor框架 Executors工厂类 newFixedThreadPool创建一个可重用的固定线程数的线程池,以共享的无界队列方式来运行这些线程,当活动线程到达固定数量时,再次加入任务会加入队列等待其他活动线程运行结束,再执行
2019-08-24
  目录