Java并发编程实战笔记(7)-线程池的使用

Posted by zhidaliao on May 22, 2016

This document is not completed and will be updated anytime.

概念

依赖性任务: 提交给线程池的任务需要依赖其他任务

只有当任务都是同类型的并且互相独立,线程池的性能才能发挥更大的作用

  • 长时间运行的任务和短时间运行的任务混在一起,除非线程池很大,不然有可能会造成阻塞
  • 如果存在依赖性任务,除非线程池无限大,不然会造成死锁(依赖任务在等待队列中)

线程饥饿死锁: 线程中存在任务需要无限期的等待池中的其他任务才能提供的资源或条件。

运行时间较长的任务

  • 会造成线程池阻塞
  • 增加运行时间较短的任务服务时间

线程池数量小于稳定状态下执行时间较长的任务数量,那么到最后可能线程池都在执行时间较长的任务, 影响响应性

解决方案

  • 限定任务等待资源的时间,不要无限制的等待 ; 平台类库的大多数可阻塞方法中都有限时版本: Thread.join / BlockingQueue.put / CountDownLatch.await
  • 如果等待超时,则把任务标记为失败,终止任务或者随后运行

当线程池中经常都是被阻塞的任务,也说明线程池太小

线程池的大小: 过大:竞争不多的CPU和内存 过小:资源空闲

考虑的因素:

  • 硬件设施
  • 任务类型: 计算密集型 、 IO密集型
  • 任务类型是否相同:不同就要使用不同的线程池

计算密集: N_thread = N_cpu + 1 其他:N_thread = N_cpu * CPU利用率 * (1 + 任务等待时间 / 任务计算时间)

ThreadPoolExecutor 是一个灵活的、稳定的线程池,允许各种定制

  • newFixedThreadPool: 固定大小的基本大小和最大大小
  • newCachedThreadPool: 最大大小设置为 Integer.Max_Value,基本大小设置为零,超时时间1分钟,需求降低时自动伸缩
  • newSingleThreadPool:单线程Executor是一种特殊的线程池,不会有任务并行执行, 因为通过线程封闭来实现线程安全
newFixedThreadPool 解析

超过设置大小, Executor 会通过队列储存新请求, 但是请求速率过快也会造成资源耗尽

任务排队有三种:

  • 有界队列:避免资源被耗尽,队列填满的策略比较麻烦,队列大小和线程池大小相关
  • 无界队列:newFixedThreadPool / newSingleThreadPool
  • 同步移交(Synchronous Handoff): newCachedThreadPool ,只有无界队列和允许拒绝新任务的情况下才有实际价值,SynchronousQueue其实不是一个队列,而是一种机制:有新的元素放入 SynchronousQueue中,必须有线程等待消费, 不然创建新线程或者丢弃新任务

控制任务的顺序可以使用 PriorityBlockingQueue ,通过自然顺序或Comparator来定义的

饱和策略
  • AbortPolicy: 抛出未检查异常 RejectedExecutionException
  • CallerRunsPolicy: 所有线程在运行,队列满了,下一个任务在执行 execute的主线程中执行,主线程一段时间内不能提交任务,不调用accept方法,之后到达的队列会保存在TCP层,持续过载会导致TCP的请求队列满了,然后对外拒绝新的请求,向外蔓延,平缓的降低性能
  • DiscardPolicy: 悄悄抛弃任务
  • DiscardOldestPolicy: 抛弃下一个即将被执行的任务,尝试重新提交新的任务(如果和优先级队列结合会导致抛弃优先级最高的任务)

工作队列满了并且没有预定义的饱和策略来阻塞 execute ,可以通过使用信号量 Semaphore 来限制任务的到达率

需要利用安全策略: 通过 Executor 中的 privilegedThreadFactory, 通过他创建的线程将于 创建 privilegedThreadFactory 的线程拥有相同的权限、 AccessControlContext / contextClassLoader

在调用完 ThreadPoolExecutor 的构造函数之后,然后可以通过设置函数来修改属性 , 基本大小 、 存活时间 、 线程工程 、 拒绝执行处理器Rejected Execution Handler

ThreadPoolExecutor 是可以通过在子类中覆盖方法扩展的

如果需要等待一个任务集并且等待他们完成,可以使用 ExecutorService.invokeAll ,在任务都执行完成之后调用 CompletionService 来获取结果(第5章)

串行工作比较独立,并且任务的工作量比 管理一个新任务带来的开销多, 适合并行化