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

本文深入探讨Java线程池的使用,包括任务与执行策略的耦合、线程饥饿死锁、运行时间较长任务的影响、线程池大小设置、ThreadPoolExecutor的配置,如线程创建销毁、任务队列管理、饱和策略、线程工厂及扩展点,最后讨论了递归算法的并行化策略。
摘要由CSDN通过智能技术生成

8.1 在任务与执行策略之间的隐形耦合

只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。

8.1.1 线程饥饿死锁

在池中,如果任务A依赖其他任务B,任务B无法完成或者被阻塞,那么可能产生死锁,称为线程饥饿死锁。如果线程池足够大,可能不会死锁。
每当提交了一个有依赖性的Executor任务时,要知道可能会出现线程的饥饿死锁,因此需要配置线程池的大小限制或配置限制。

8.1.2 运行时间较长的任务

如果任务阻塞的时间过长,即使无死锁,线程池的响应性也会变得糟糕。执行时间长的任务不仅会造成线程池的堵塞,还会增加执行时间较短任务的服务时间。
限定任务等待资源的时间
如果线程池中总充满被阻塞的任务,可能说明规模太小了。

8.2 设置线程池的大小

线程池的理想大小取决于被提交任务的类型以及所部署系统的特性,不要根据代码固定配置,应该根据某种配置机制,或者根据Runtime.availableProcessors来动态计算。

如果需要执行不同类型的任务,并且它们之间的行为差异相差很大,那么应该考虑多个线程池。,从而是每个线程池根据自己的工作负载来调整。

  • 计算密集型 Ncpu+1
  • IO密集型

8.3 配置ThreadPoolExecutor

8.3.1 线程的创建与销毁

线程池的基本大小(没有任务执行时线程池的大小)、最大大小以及存活时间等因素共同负责线程的创建于销毁。

默认值:基本大小=0,最大大小=Integer.MAX_VALUE,超时=1min
特点:可无限扩展,需求降低时会自动回收。

8.3.2 管理队列任务

高负载时,即使使用线程池,仍有可能耗尽资源,例如请求速率超过了服务器的处理速率,Executor管理的Runnable队列会持续变大。这样不仅耗尽内存,还会使响应性能变低。

ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务,队列的选择和其他配置有关,例如线程池大小:

  • 无界队列
  • 有界队列
  • 同步移交Synchronous Handoff

newFixedThreadPool和newSingleThreadExecutor在默认情况下使用一个无界的LinkedBlockingQueue,如果任务到达的速度超过了线程池的处理速度,队列将无限制的增加。

更稳妥的资源管理策略是使用有界队列:ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue。可以避免资源耗尽,同时带来饱和策略Saturation Policy

使用有界队列时,队列的大小与线程池的大小必须一起调节。

对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。
SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。**使用直接移交更高效。当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值,在newCachedThreadPool工厂方法中使用了SynchronousQueue。

newCachedThreadPool是一种很好的默认选择,他能提供比固定大小的线程池更好的排队性能。当需要限制当前任务的数量以满足资源管理需求时,可以选择固定大小的线程池,否则容易发生过载。

**只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果存在依赖性,那么有界的线程池或队列可能导致线程饥饿死锁问题,此时应该使用无界的线程池,例如newCachedThreadPool。

8.3.3 饱和策略

当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor可以通过调用setRejectedExecutioHandler来修改。如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略。

  • AbortPolicy
    默认策略,该策略将抛出未检查的RejectedExecutionException,调用者可以捕获自行处理。抛弃最旧的(Discard-Oldest)将会抛出下一个将被执行的任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么将会抛弃优先级最高的任务,因此不要将抛弃最旧的饱和策略和优先队列一起使用)
  • CallerRunsPolicy
    当线程池占满,并且工作队列被填满,下一个任务会在调用executor时在主线程中执行。因此主线程至少在一段时间内不能提交任何任务,从而使得工作者线程有时间来处理正在执行的任务。
    在这期间,主线程不会调用accept,因此到达的请求将被保存在TCP层的队列中而不是在应用程序的队列中。如果持续过载,TCP层将最终发现他的请求队列被填满,因此开始抛弃请求。当服务器过载时,这种过载情况会向外蔓延——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。

可以使用信号量Semaphore来控制任务的提交速率。

8.3.4 线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。默认的线程工厂方法将创建一个新的、非守护的线程,并且不包含特殊的配置信息。通过制定一个线程工厂方法,可以定制线程池的配置信息。

public interface ThreadFactory {
	Thread newThread(Runnable r);
}

8.3.5 在调用构造函数后再定制ThreadPoolExecutor

在调用完ThreadPoolExecutor的够赞函数后,仍然可以通过设置函数来修改大多数传递给他的构函数的参数。如果Executor是通过Executors中的某个工厂方法创建的,可以将结果转换为ThreadPoolExecutor以访问设置器。(newSingleThreadExecutor除外)为什么???

ExecutorService exec = Executors.newCachedThreadPool();
if (exec instanceof ThreadPoolExecutor)
	((ThreadPoolExecutor) exec).setCorePoolSize(10);
else
	throw new AssertionError("Oops, bad assumption");

在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其之暴露出ExecutorService的方法,因此不能对他进行配置。newSingleThreadExecutor就是如此。可以在自己的Executor中使用这项技术防止执行策略被修改。
疑问:newSingleThreadExecutor可以修改线程池大小吗?看下源码实验下。

8.4 扩展ThreadPoolExecutor

  • beforeExecute
    如果beforeExecute抛出一个RuntimeException,那么任务将不被执行,并且afterExecute也不会被调用。
  • afterExecute
    无论任务从run中正常返回,还是抛出一个异常,都会被调用。相反,如果任务完成后带有一个error,那么就不会调用AfterExecute。
  • terminated
    所有任务完成并且线程关闭,可以用来释放Executor在其生命周期中分配的各种资源。此外还可以发送通知,记录日志,收集等。

8.5 递归算法的并行化

当串行循环中的各个迭代操作之间彼此独立,并且每个迭代操作执行的工作量比管理一个新任务时带来的开销更多,那么这个串行循环就适合并行化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值