Crab213's Blog.

Java并发编程实践笔记6:线程池的应用

2016/04/02

任务与执行策略的隐式耦合

我们心目中的理想任务模型是任务之间是相互独立的,这样每个任务就可以独立执行。因此执行这样的任务的线程池的执行策略和任务直接是没有耦合的。我们可以任意选择执行策略,线程池中线程的数目也不受任何制约。

但是实际中还是有很多任务不是互相独立的,或者有其他特殊需求,这就会使任务和其执行策略有所联系,不是完全独立的。

  • 非独立任务。某个任务可能会依赖其他任务的执行结果,或者起执行后发生的副作用。这样的任务会造成活性(liveness)问题,会限制线程池的大小。
  • 使用了线程封闭的线程。使用线程封闭从而避免同步的线程自然而然需要顺序执行。即必须使用单线程的线程池。
  • 使用ThreadLocal的线程。ThreadLocal是一个基于线程的容器。但是Executor并不会在一个任务执行完成后销毁线程,而是会在合适的时候复用该线程。

线程饥饿死锁(Thread Starvation Deadlock)

非独立的线程会造成死锁。考虑某个单线程线程池,如果一个线程将另一个线程递交给线程池,并等待那个线程的执行结果,这会造成死锁,因为线程池本身就一个线程,他会等待一个线程结束时才会执行第二个线程。当一个限定了大小的线程池执行这种非独立线程的时候,会造成这种类型的死锁。

长时间运行的任务

如果线程池中运行的任务会发生阻塞。如果线程池大小比较小,那么线程池最终可能会被这些长时间运行的任务填满,造成响应问题。一个直接的解决方法是不要进行无限的等待,一些阻塞方法都带有可以设置超时的版本,比如Thread.joinSelector.select等等。

如果决定线程池大小

线程池的理想大小取决于线程池执行的任务及程序的部署环境。线程池大小不应该被硬编码在程序中,我们需要提供一种可以配置线程池大小的方式或者让程序根据Runtime.availableProcessors来动态决定线程池的大小。

实际上我们并不需要精确估量最优的线程池大小,只需要确保它不适过大或者过小就行了。线程池过大会使线程竞争加重,加大系统资源(内存,调度间隔时间)消耗。而线程池大小过于小又会造成处理器闲置,使程序吞吐量受到影响。

为了正确设置线程池的大小,我们需要彻底了解你的运行环境,包括你有多少个处理器,你有多少内存,你要运行的任务是计算任务还是I/O相关,或者两者混合,它们需要某些稀缺资源么,比如JDBC连接。如果你的线程池中执行的任务可以很明显的分成几类,那么你可依考虑使用多个线程池。

对于计算任务,n个处理器的系统的最优配置通常为n+1个线程。对于I/O相关的任务,你需要更多的线程,毕竟这些任务可能会阻塞,它们在同一时刻不会全部参加调度。

定义:

  • Ncpu = CPU的数目
  • Ucpu = 我们想要的CPU占用比率
  • W/C = 计算占用的时间与等待占用的时间的比率

那么我们可以得到最优的线程池大小:

Nthreads = Ncpu Ucpu (1 + W/C)

当然,其他资源也会限制线程池大小,比如文件句柄,socket句柄,数据库连接等等。

CATALOG
  1. 1. 任务与执行策略的隐式耦合
  2. 2. 线程饥饿死锁(Thread Starvation Deadlock)
  3. 3. 长时间运行的任务
  4. 4. 如果决定线程池大小