并发编程之线程池

本篇文章介绍线程池,内容总结摘抄自《Java并发编程的艺术》和《Java并发编程实战》,仅作笔记。

线程池

线程池,是指管理一组同构工作线程的资源池。线程池是与工作队列(Work Queue)密切相关的,其中在工作队列中保存了所有等待执行的任务。工作线程(Work Thread)的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。

“在线程池中执行任务”比“为每个任务分配一个线程”优势更多。通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。另外,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态,同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。使用线程池还可以对线程统一分配、调优和监控。

线程池的主要处理流程如下图:

  1. 提交任务后,首先判断核心线程池的线程数量是否与构造函数中传入的核心线程池大小相同。如果否,则创建新的线程来执行此任务;如果不是,则进入步骤2。
  2. 判断工作队列是否已满(此处只针对有界阻塞队列)。如果没有满,则将任务插入到工作队列中;如果满了,则进入步骤3。
  3. 判断线程池的线程数量是否与构造函数中传入的maximumPoolSize相同。如果不相同,则创建新的临时线程来执行任务;如果相同,则根据饱和策略处理这个任务。

ThreadPoolExecutor执行executor()方法的示意图如下:

  1. 当前运行的线程数量少于corePoolSize,则创建新线程来执行任务,这一步骤需要获取全局锁。
  2. 如果运行的线程等于或多余corePoolSize,则将任务加入BlockingQueue。
  3. 如果BlockingQueue已满,则创建新的临时线程来处理任务,这一步骤需要获取全局锁。
  4. 如果当前运行的线程数量与maximumPoolSize相同,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法执行饱和策略。

ThreadPoolExecutor采用上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

线程池的使用

我们可以通过ThreadPoolExecutor来创建一个线程池,关于ThreadPoolExecutor可以看这篇文章。创建成功之后,可以使用两个方法向线程池中提交任务,分别为execute()方法和submit()方法。

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。execute()方法的定义如上,需要传入一个Runnable。

public void execute(Runnable command);

submit()方法用于提交需要返回值的任务。线程池会返回一个Future,通过这个Future可以判断对象是否执行成功,并且可以通过Future.get()方法获取返回值,get()方法会阻塞当前线程直到任务完成。关于Future可以看这篇文章

public <T> Future<T> submit(Runnable task, T result);
public <T> Future<T> submit(Callable<T> task);
public Future<?> submit(Runnable task);

可以通过调用shutdown()方法或者shutdownNow()方法来关闭线程池。它们的原理是遍历线程池中的工作线程,逐个调用线程的interrupt()方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是这两个方法存在一定的区别,shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表;而shutdown方法只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有在执行的任务的线程。

只要调用这两个方法中的任意一个,isShutdown()方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminated()方法才会返回true。

合理地配置线程池

要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度分析:

  • 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
  • 任务的优先级:高、中和低。
  • 任务的执行时间:长、中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置CPU数量+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*CPU数量。混合型的任务,如果可以拆分,将其拆分为一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行时间相差不是太大,分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲的时间就越长,因此线程数应该设置的越大,这样才能更好地利用CPU。

建议使用有界队列。有界队列能增加系统的稳定性和预警能力。因此尽量直接用ThreadPoolExecutor的构造函数创建线程池,而不是用工具类Executors,因为Executors创建FixedThreadPool和SingleThreadPool使用的工作队列是无界队列LinkedBlockingQueue,允许的长度为Integer.MAX_VALUE,在提交任务的速度大于处理任务速度的情况下,可能会堆积大量的任务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值