Java并发编程实战 第8章 线程池的使用

合理的控制线程池的大小:

下面内容来自网络。不过跟作者说的一致。不想自己敲了。留个记录。

要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:

任务的性质:CPU密集型任务、IO密集型任务、混合型任务。

任务的优先级:高、中、低。

任务的执行时间:长、中、短。

任务的依赖性:是否依赖其他系统资源,如数据库连接等。

性质不同的任务可以交给不同规模的线程池执行。

对于不同性质的任务来说,CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数,IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1,而对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。

若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。 
当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。

在这篇如何合理地估算线程池大小?文章中发现了一个估算合理值的公式

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

可以得出一个结论: 
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 
以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。

并发编程网上的一个问题 
高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池? 
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换 
(2)并发不高、任务执行时间长的业务要区分开看: 
  a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务 
  b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换 
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

 

我们可以通过如下代码来获取处理器的数量:

//获取处理器的数量

System.out.println(Runtime.getRuntime().availableProcessors());

管理队列任务

ThreadPoolExecutor是ExecutorService的实现类。我们使用Executors.newCacheThreadPoo这些方法返回的对象就是已经定制好的ThreadPoolExecutor对象。

ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。

基本的任务排队方法有三种:

  • 有界队列
  • 无界队列
  • 直接提交

为了便于跨大量上下文使用,此类提供了很多可调整的参数和扩展挂钩。但是,强烈建议程序员使用较为方便的 Executors 工厂方法 Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)和 Executors.newSingleThreadExecutor()(单个后台线程),它们均为大多数使用场景预定义了设置。否则,在手动配置和调整此类时,使用以下指导(这里只罗列了部分我关注的 API中有更多):

  • 排队

所有 BlockingQueue 都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:

排队有三种通用策略:

直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。如:Executors.newCachedThreadPool()就是使用的这个。

无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。 Executors.newFixedThreadPool(int)(固定大小线程池)和 Executors.newSingleThreadExecutor()就是使用的这个。

有界队列。有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。 如果有界队列慢了,新的任务来了,就会根据预先设计好的饱和策略来处理。

  • 挂钩方法

此类提供 protected 可重写beforeExecute(java.lang.Thread, java.lang.Runnable)afterExecute(java.lang.Runnable, java.lang.Throwable) 方法,这两种方法分别在执行每个任务之前和之后调用。它们可用于操纵执行环境;例如,重新初始化 ThreadLocal、搜集统计信息或添加日志条目。此外,还可以重写方法 terminated() 来执行 Executor 完全终止后需要完成的所有特殊处理。

如果挂钩或回调方法抛出异常,则内部辅助线程将依次失败并突然终止。

  • 队列维护

方法 getQueue() 允许出于监控和调试目的而访问工作队列。强烈反对出于其他任何目的而使用此方法。remove(java.lang.Runnable)purge() 这两种方法可用于在取消大量已排队任务时帮助进行存储回收。

配置通过本标准方法返回的ThreadPoolExecutor

  1. public static void main(String[] args) {
  2.        ExecutorService es = Executors.newCachedThreadPool();
  3.        System.out.println(es instanceof ThreadPoolExecutor);
  4.        ThreadPoolExecutor tpe = (ThreadPoolExecutor)es;
  5.        tpe.setKeepAliveTime(1, TimeUnit.SECONDS);
  6.    }

上面是ThreadPoolExecutor的一个用途,其实可以完全自定义自己的ThreadPoolExecutor,不使用Executors.new生成的逻辑。事实上,Executors.new生成的就是配置好的ThreadPoolExecutor。

posted on 2017-06-23 01:09  张小贱1987  阅读(866)  评论(0编辑  收藏  举报

导航