从零开始学多线程之自定义配置线程池(七)

 

等待其他资源,可能会产生线程饥饿死锁

在线程池中如果一个任务依赖于其它任务的执行,就可能产生死锁.在一个单线程化的Executor中,提交两个任务,任务二滞留在工作队列中等待第一个任务完成,但是第一个任务不会完成,因为它在等待第二个任务的完成(需要第二个任务执行的结果进行运算),这就会发生死锁.

在一个大的线程池中,如果所有线程执行的任务都阻塞在线程池中,等待着仍然处于同一工作队列中的其它任务,那么会发生同样的问题.这被称作线程饥饿死锁(thread starvation deadlock)

产生死锁的情况: 只要池任务开始了无限期的阻塞,其目的是等待一些资源或条件,此时只有另一个池任务的活动才能使那些条件成立,比如等待返回值.除非你能保证这个池足够大,否则会产生线程饥饿死锁.

池任务等待另一个池任务的结果,可能会发生死锁:

public class ThreadDeadLock {      ExecutorService exec = Executors.newSingleThreadExecutor();       public  class Task implements Callable{          @Override         public Object call() throws Exception {             //等待另一个池任务的结果             Future<String> future1 = exec.submit(new LockTask());             Future<String> future2 = exec.submit(new LockTask());             //可能发生死锁             return future1.get()+future2.get();          }     }      
无论何时,你提交了一个非独立的Executor任务,要明确出现线程饥饿死锁的可能性,并且,在代码或者配置文件以及其他可以配置Executor的地方,任何有关池的大小和配置约束都要写入文档

耗时的任务,设置超时时间

如果你的线程池不够大,又有很多耗时的任务,这会影响服务的响应性.这时候你可以限定任务等待资源的时间,而不是无限制地等下去.

耗时的任务可能会死锁或者响应的很慢

大多数平台类库中的阻塞方法,都有限时和非限时两个版本.例如Blocking.put.如果超时你可以把任务标记为失败,终止或者把他重新返回队列,准备之后执行.这样无论每个任务的最终结果是成功还是失败,都保证了任务会向前发展,这样可以更快地将线程从任务中解放出来.(如果线程池频频被阻塞的任务充满,这同样可能是池太小的一个信号).

定制线程池的大小

不要硬编码线程池的大小

线程池合理的长度取决于未来提交的任务类型和所部属系统的特征.池的长度应该由某种配置机制来提供,或者利用Runtime.availableProcessors(获取你电脑的处理器数量),动态进行计算

线程池过大&过小的坏处

线程池过大: 线程对稀缺的CPU和内存资源的竞争,会导致内存的高使用量.线程间切换也会消耗资源

线程池过小:由于存在很多可用的处理器资源没用,会对吞吐量造成损失

制定线程池大小依据的内容

正确的定制线程池的长度,需要理解你的计算环境、资源预算和任务的自身特性.

部署系统中安装了多少个CPU?多少内存?任务主要执行的是计算、I/O还是一些混合操作?它们是否需要像JDBC Connection 这样的稀缺资源?

如果你有不同类别的任务,它们拥有差别很大行为,那么请考虑使用多个线程池,这样每个线程池可以根据不同任务的工作负载进行调节.

计算密集型和I/O密集型的线程选择

计算密集型:一直在计算,cpu利用率高,过多的线程没有意义,反而切换线程会消耗额外的资源.

I/O密集型:例如查找数据库,等待数据造成的阻塞,CPU利用率低,多个线程可以提高响应速度.

对于计算密集型的任务,一个有N个处理器的系统通常使用一个N+1个线程的线程池来获得最优的利用率(计算密集型的线程恰好在某时因为发生一个页错误或者因其它原因而暂停,刚好有一个"额外"的线程,可以确保在这种情况下CPU周期不会中断工作).

对于包含了I/O和其他阻塞操作的任务,不是所有的线程都会在所有的时间被调度,因此你需要一个更大的池.

在一个基准负载下,可以使用不同大小的线程池运行你的应用程序,并观察CPU利用率的水平.

计算线程池大小的公式

N = CPU的数量

U = 目标CPU的使用率,介于0-1之间

W/C = 等待时间(wait)和计算时间(calculate)的比率

为保持处理器到达期望的使用率,最优的池的大小等于:

num(线程数) = N * U * (1 + W/C);

你可以使用Runtime来获得CPU的数目:

int nCpus = Runtime.getRuntime().availableProcessors();

简单的例子

通过Runtime.getRuntime().availableProcessors();得到我的电脑cpu数是4, 我期望cpu的使用率是100%,假设等待时间是10秒,计算时间是1秒.那么我最优的池大小就是:

4 * 100% * (1+10/1) = 44

线程池的长度和资源池的长度互相影响

当任务需要使用池化的资源时,比如数据库链接,线程池的长度和资源池的长度会互相影响.

如果每一个任务都需要一个数据库链接,那么连接池的大小就限制了线程池的有效大小;类似地,当线程池中的任务是连接池的唯一消费者时,那么线程池的大小反而又会限制了连接池的有效大小.


配置ThreadPoolExecutor

灵活配置ThreadPoolExecutor

使用Executors工厂方法可以创建各种类型的线程池,newCachedThreadPool、newFixedThreadPool和newScheduledThreadExecutor等.如果这些执行策略不能满足你的需求,你可以 new ThreadPoolExecutor(传递各种参数)来配置.

ThreadPoolExecutor有很多构造函数
image

最后一个构造函数是功能最全的,也是最常用的,源码:

 public ThreadPoolExecutor(int corePoolSize,                               int maximumPoolSize,                               long keepAliveTime,                               TimeUnit unit,                               BlockingQueue<Runnable> workQueue,                               ThreadFactory threadFactory,                               RejectedExecutionHandler handler) {         if (corePoolSize < 0 ||             maximumPoolSize <= 0 ||             maximumPoolSize < corePoolSize ||             keepAliveTime < 0)             throw new IllegalArgumentException();         if (workQueue == null || threadFactory == null || handler == null)             throw new NullPointerException();         this.acc = System.getSecurityManager() == null ?                 null :                 AccessController.getContext();         this.corePoolSize = corePoolSize;         this.maximumPoolSize = maximumPoolSize;         this.workQueue = workQueue;         this.keepAliveTime = unit.toNanos(keepAliveTime);         this.threadFactory = threadFactory;         this.handler = handler;     } 

它有五个参数分别是:

  • corePoolSize 核心池大小
  • maximumPoolSize 最大池大小
  • keepAliveTime 存活时间
  • TimeUnit 时间单元
  • BlockingQueue 工作队列
  • ThreadFactory 线程工厂
  • RejectedExecutionHandler 拒绝执行处理器

注意源码,限定了设置这几个值的范围,不满足就会报非法参数异常,当时博主就是将核心池的值设置比最大池的值大,报了这个异常,看了源码才晓得:

 if (corePoolSize < 0 ||             maximumPoolSize <= 0 ||             maximumPoolSize < corePoolSize ||             keepAliveTime < 0)             throw new IllegalArgumentException();

corePoolSize(核心池大小)、maximum pool size(最大池的大小)和存活时间(keep-alive time)共同管理着线程的创建与销毁.

corePoolSize: 线程池的实现试图维护池的大小(Eexcutors.newSingleThreadExecutor就是一种实现);即时没有执行任务,池的大小也等于核心池的大小,工作队列充满后会创建更多的线程.(Executors.newCacheThreadPool池的大小就是不固定的,随着任务增减线程的数量).

maximumPoolSize:最大池的大小制约着线程池可以同时执行的的最大线程数(限制并发数量),如果一个线程闲置的时间超过了存活时间就会被回收.并且同时运行的线程的数量不能超过核心池大小,线程池会终止超过的数量.

keep alive time & TimeUniht: 存活时间保证了空闲的线程会被回收,这样释放了这部分被占有的资源可以做更有用的事情.

两种特殊的线程池实现

Executors.newFixedThreadPool和newCachedThreadPool是两种特殊的实现.

50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信