Java线程之线程池

本文探讨了Java线程池的使用,强调了线程饥饿死锁、运行时间较长任务的影响以及线程池大小的设置。线程池的大小应当根据任务类型和系统资源进行适当配置,以避免性能下降和死锁。此外,介绍了如何配置ThreadPollExecutor,包括线程的创建与销毁、任务队列管理和饱和策略,以确保线程池的高效和稳定运行。
摘要由CSDN通过智能技术生成

Executor框架可以将任务的提交与任务的执行策略解耦开来。就像许多对复杂过程的解耦操作那样,这种论断多少有些言过其实了。虽然Executor框架为定制和修改执行策略提供了相当大的灵活性,但并非所有的任务都能适用所有的执行策略。有些类型的任务需要明确的指定执行策略。

依赖性任务 使用线程封闭机制的任务 对相应时间敏感的任务 使用ThreadLocal的任务
大多数行为正确的任务都是独立的:它们不依赖于其他任务的执行时序,执行结果或其他效果。当线程池中执行独立任务时,可以随意地改变线程池的大小和配置,这些修改只会对执行性能产生影响。然而如果提交给线程池的任务需要依赖其他任务,那么就隐含地给执行策略带来约束,此时必须小心的维持这些执行策略以避免产生活跃性问题 与线程池相比,单线程的Executor能够对并发性做出更强的承诺。它们能够确保任务不会并发的执行,使你能够放宽代码对线程安全的要求。对象可以封闭在任务线程中,使得该线程中执行的任务在访问该对象时不需要同步。即使这些资源不是线程安全的也没有问题。这种情形将在任务与执行策略之间形成隐式的耦合——任务要求其执行所在的Executor是单线程的。如果将Executor从单线程环境改变为线程池环境,那么将会失去线程安全性。 GUI应用程序对于相应时间是敏感的:如果用户在点击按钮后需要很长延迟才能得到可见的反馈,那么他们会感到不满。如果将一个运行时间较长的任务提交到单线程的Executor中,或者将多个运行时间较长的任务提交到一个只包含少量线程的线程池中,那么将降低由该Executor管理的服务的响应性 ThreadLocal使每个线程都可以拥有某个变量的一个私有“版本”。然而,只要条件允许,Executor可以自由地重用这些线程。在标准的Executor实现中,当执行需求较低时将回收空闲线程,而当需求增加时将添加新的线程,并且如果从任务中抛了一个未受检查的异常,那么将一个新的工作者线程来替代抛出异常的线程。只有当线程本地值得生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线程池的线程中不应该使用ThreadLocal在任务之间传值

 只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的任务与运行时间较短的任务混合在一起,那么除非线程池很大,否则有可能造成“拥塞”。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则有可能造成死锁。幸运的是,在基于网络的典型服务器应用程序中——网页服务器、邮件服务器以及文件服务器等,它们的请求通常都是同类型的并且相互独立的。

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

线程饥饿死锁

 在线程池中,如果任依赖于其他任务,那么可能产生死锁。在单线程的Executor中,如果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交任务的结果,那么通常会引发死锁。第二个任务停留在工作队列中,并等待第一个任务的完成,而第一个人任务又无法完成,因为他在等待第二个任务的完成。在更大的线程池中,如果所有正在执行任务的线程都由与等待其他仍处于工作队列中的任务而阻塞,那么将会发生同样的问题。这种现象被称为线程饥饿死锁,只要线程池中的任务需要无限地等待一些必须由池中其他任务才能提供的资源或条件,例如在某个任务等待另一个任务返回值或者执行结果,那么除非线程池足够大,否则将会发生线程饥饿死锁。

每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程“饥饿”死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。

 除了在线程池大小上的显示限制外,还可能由于其他资源上的约束而存在一些隐式限制。如果应用程序使用一个包含10个连接的JSBC连接池,并且每个任务需要一个数据库连接,那么线程池就好像只有10个线程,因为当超过10个任务时,新的任务需要等待其他任务释放连接。

运行时间较长的任务

 如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。执行时间较长的任务不仅会造成线程池堵塞,甚至还会增加执行时间较短的任务的服务时间。如果线程池中的线程数量远小于在稳定状态下执行时间较长任务的数量,那么到最后可能所有的线程都会运行这些执行时间较长的任务,从而影响整体的响应性。
 有一项技术可以缓解执行时间较长任务造成的影响,即限定任务等待资源的时间,而不要无限制的等待。在平台类库的大多数阻塞方法中,都同时定义了限时版本和无限时版本,例如Thread.joinBlockingQueue.putCountDownLatch.await以及Selector.selector等。如果等待超时,那么可以将任务标识为失败。然后终止任务或者将任务重新放回队列以便随后执行。这样,无论任务的最终结果是否成功,这种办法都能确保任务总能继续执行下去,并将线程释放出来,执行一些能更快完成的任务。如果在线程池中总是充满了被阻塞的任务,那么也有可能表明线程池的规模过小。

设置线程池的大小

 线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中通常不会固定线程池的大小,而应该通过某种配置机制来提供,或根据Runtime.availableProcessors来动态计算大小。
 幸运的是,要设置线程池的大小也并不困难,只需要避免“过大”和“过小”这两种极端情况。如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还有可能耗尽资源。如果线程过小,那么将会导致许多空闲的处理器无法执行工作,从而降低吞吐量。
 要想正确的设置线程池的大小,必须分析计算环境、资源预算和任务的特性。在部署的系统中有多少个CPU?多大的内存?任务是计算密集型、I/O密集型还是二者皆可?它们是否需要像JDBC连接这样的稀缺资源?如果需要执行不同类别的任务,并且它们之间的行为相差很大,那么应该考虑使用多个线程池,从而使每个线程池可以根据自己的工作负载来调整。
 对于计算密集型的任务,在拥有N(cpu)个处理器的系统上,当线程池的大小为N(cpu)+1时,通常能实现最优的利用率。(即使当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保CPU的时钟周期不会被

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值