并发编程实战学习笔记(六)——线程池的使用

任务与执行策略之间的隐性耦合

依赖性任务

当在线程池中执行独立的任务时,可以随意地改变线程池的大小和配置,这些修改只会对执行性能产生影响。如果提交给线程池的任务需要依赖其它的任务,那么就隐含地给执行策略带来了约束,此时必须小心地维持这些执行策略以避免产生活跃性问题“线程饥饿死锁”。

使用线程封闭机制的任务

如果将Executor从单线程环境改为线程池环境,那么将会失去线程安全性。

对响应时间敏感的任务

如果将一个运行时间较长的任务提交到单线程的Executor中,或者将多个运行时间较长的任务提交到一个只包含少量线程的线程池中,那么将降低由该Executor管理的服务的响应性。

使用ThreadLocal的任务

ThreadLocal使每个线程都可以拥有某个变量的一个私有“版本”。然而,只要条件允许,Executor可以自由地重用这些线程。只有当线程本地值的生命受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线程池中不应该使用ThreadLocal在任务之间传递值。

结论

在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其它的任务,那么会要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或被拒绝,而采用线程封闭机制的任务需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用了某种不合适的执行策略而破坏安全性或活跃性。

线程饥饿死锁

定义

在线程池中,如果任务依赖于其它任务,那么可能产生死锁。在单线程的Executor中,如果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交任务的结题,那么通常会引发死锁。如果所有正在执行任务的线程都由于等待其它仍处于工作队列中的任务而阻塞,那会发生同样的问题,这种现象被称为线程饥饿死锁。

可能触发的两种情况

  • 提交有依赖性的executor任务,都需要注意有可能会产生饥饿死锁
  • 除了线程池大小的显式限制外,其它资源上的约束而存在一些隐匿限制,有可能间接触发产生类似的“饥饿”。如每个线程都需要一个JDBC连接池,那线程池就好像只有10个线程。因为超过10个任务时,新的任务需要等待其它任务释放连接。

运行时间较长的任务使用线程池注意事项

有限线程池线程可能会被执行时间长任务占用过长时间,最终导致执行时间短的任务也被拉长了“执行”时间。可以考虑限定任务等待资源的时间,而不要无限制地等待。

线程池大小考虑因素

  • 分析计算环境、资源预算和任务的特性。在部署的系统中有多少个CPU?多大的内存?任务是计算密集型、I/O密集型还是二者皆可。
  • 对于计算密集型的任务,在拥有N(CPU)个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率(即使当计算密集型的线程偶尔由于页缺失故障或者其它原因而暂停时,这个“额外”的线程也能确保CPU的时钟周期不会被浪费)
  • 对于包含I/O操作或者其它阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。
  • 线程池资源并不是唯一影响线程池大小的资源,还包括内存、文件句柄、套接字句柄和数据库连接等。

线程池中线程的创建与销毁

前记,基本逻辑

会依据配置参数,自动生成需要的线程以及销毁富余的线程,以提高系统资源的利用率

基本大小(Core Pool Size)、最大大小(Maximum Pool Size)以及存活时间等因素共同负责线程的创建与销毁。

基本大小也就是线程池的目标大小,即在没有任务执行时(初期线程并不启动,而是等到有任务提交时才启动,除非调用prestartAllCoreThreads)线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。

线程池的最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将终止。

fixedthreadpool中线程为什么不会超时?

因为基本大小与最大大小一致,按照上述销毁逻辑,是不会终止线程的

cachedthreadpool是如何实现队列未满时就开始创建线程的

通过看代码发现,execute函数调用的时候,会判断当前是否有空闲的线程存在,如果没有,就会创建一个新线程

高并发情况下线程池可能存在的问题

问题一:如果使用cachedthreadpool,无限制创建线程,那么将导致不稳定性,比如达到最高线程允许数量,内存被用光等而报错。

解决办法:这种情况可以采用固定大小的线程池(而不是每收到一个请求就创建一个新线程)来解决这个问题。

问题二:高负载时,尽管使用固定线程池,仍可能因为无限制任务队列而耗尽资源,只是出现问题的概率较小。如果新请求的到达速率超过了线程池的处理速率,那么新到来的请求将被累积起来。

解决办法:使用有界队列可以防止资源耗尽,但也因此必须要考虑饱和策略。因为默认的中止策略可能不是我们想要的,详情参考下面饱和策略

基本的任务排队方法有3种


  • 无界队列 在fixedthreadpool和singlethreadpool中有使用LinkedBlockedQueue
  • 有界队列 LinkedBlockedQueue和ArrayBlockedQueue都支持
  • 同步移交 如SynchronousQueue,在cachedthreadpool中使用

同步移交队列使用时有一定的限制,只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。

饱和策略

ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改


  • 中止(Abort)策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。
  • 抛弃(Discard)策略会悄悄抛弃该任务。
  • 抛弃最旧的(Discard-Oldest)策略则会抛弃下一个将被执行的任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将“抛弃最旧的”饱和策略和优先级队列放在一起使用)
  • 调用者运行(Caller-Runs)策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者。当线程池中所有线程都被占用,并且工作队列被填满后,下一个任务会在调用executor时在主线程中执行。

当服务器过载时,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。
ThreadPoolExecutor executor = new ThreadPoolExecutor(N_THREADS,N_THREADS,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(CAPACITY));
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy);

就像YII框架一样,JDK类都提供了默认的行为,但几乎所有东西都是可定制的。线程池的任务队列,线程创建与销毁,扩展与伸缩,饱和策略都是定制可配的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值