线程池核心线程数和最大线程数总结

2年前 (2022) 程序员胖胖胖虎阿
313 0 0

一、源码doc:

java.util.concurrent public class ThreadPoolExecutor extends AbstractExecutorService

一个ExecutorService ,它使用可能的多个池线程之一执行每个提交的任务,通常使用Executors工厂方法进行配置。

线程池解决了两个不同的问题:由于减少了每个任务的调用开销,它们通常在执行大量异步任务时提供改进的性能,并且它们提供了一种限制和管理资源的方法,包括执行一组线程时消耗的线程。任务。每个ThreadPoolExecutor还维护一些基本的统计信息,例如完成任务的数量。

为了在广泛的上下文中有用,这个类提供了许多可调整的参数和可扩展性挂钩。但是,建议程序员使用更方便的Executors工厂方法Executors.newCachedThreadPool (无界线程池,具有自动线程回收功能)、 Executors.newFixedThreadPool (固定大小的线程池)和Executors.newSingleThreadExecutor (单后台线程),这些方法预先配置了最常见的使用场景。否则,在手动配置和调整此类时使用以下指南:

核心和最大池大小
ThreadPoolExecutor将根据 corePoolSize (请参阅getCorePoolSize )和 maximumPoolSize (请参阅getMaximumPoolSize )设置的边界自动调整池大小(请参阅getPoolSize )。当在方法execute(Runnable)中提交了一个新任务,并且运行的线程少于 corePoolSize 时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。如果运行的线程数多于 corePoolSize 但少于 maximumPoolSize,则仅当队列已满时才会创建新线程。通过将 corePoolSize 和 maximumPoolSize 设置为相同,您可以创建一个固定大小的线程池。通过将 maximumPoolSize 设置为基本上无界的值,例如Integer.MAX_VALUE ,您可以允许池容纳任意数量的并发任务。最典型的是,核心和最大池大小仅在构造时设置,但它们也可以使用setCorePoolSize和setMaximumPoolSize动态更改。
按需施工
默认情况下,即使是核心线程也仅在新任务到达时才最初创建和启动,但这可以使用方法prestartCoreThread或prestartAllCoreThreads动态覆盖。如果您使用非空队列构造池,您可能希望预启动线程。
创建新线程
新线程是使用ThreadFactory创建的。如果没有另外指定,则使用 E Executors.defaultThreadFactory ,它创建的线程都在同一个ThreadGroup中,并且具有相同的NORM_PRIORITY优先级和非守护进程状态。通过提供不同的 ThreadFactory,您可以更改线程的名称、线程组、优先级、守护进程状态等。如果ThreadFactory在从newThread返回 null 时未能创建线程,则执行程序将继续,但可能无法执行任何任务。线程应该拥有“modifyThread” RuntimePermission 。如果工作线程或其他使用该池的线程不具备此权限,则服务可能会降级:配置更改可能无法及时生效,关闭池可能会一直处于可以终止但未完成的状态。
保活次数
如果池当前有多个 corePoolSize 线程,多余的线程如果空闲时间超过 keepAliveTime(请参阅getKeepAliveTime(TimeUnit) )将被终止。这提供了一种在池没有被积极使用时减少资源消耗的方法。如果池稍后变得更加活跃,则将构造新线程。此参数也可以使用方法setKeepAliveTime(long, TimeUnit)动态更改。使用Long.MAX_VALUE TimeUnit.NANOSECONDS的值可以有效地禁止空闲线程在关闭之前终止。默认情况下,keep-alive 策略仅适用于超过 corePoolSize 线程的情况。但是方法allowCoreThreadTimeOut(boolean)也可用于将此超时策略应用于核心线程,只要 keepAliveTime 值不为零即可。
排队
任何BlockingQueue都可以用来传输和保存提交的任务。此队列的使用与池大小交互:

  • 如果运行的线程少于 corePoolSize,则 Executor 总是更喜欢添加新线程而不是排队。
  • 如果 corePoolSize 或更多线程正在运行,Executor 总是更喜欢排队请求而不是添加新线程。
  • 如果请求无法排队,则会创建一个新线程,除非这将超过 maximumPoolSize,在这种情况下,该任务将被拒绝。

排队的一般策略有以下三种:

1.直接交接。工作队列的一个很好的默认选择是SynchronousQueue ,它将任务交给线程而不用其他方式保留它们。在这里,如果没有立即可用的线程来运行任务,则尝试将任务排队将失败,因此将构造一个新线程。在处理可能具有内部依赖关系的请求集时,此策略可避免锁定。直接切换通常需要无限的 maximumPoolSizes 以避免拒绝新提交的任务。这反过来又承认了当命令的平均到达速度快于它们的处理速度时,线程无限增长的可能性。
2.无界队列。当所有 corePoolSize 线程都忙时,使用无界队列(例如没有预定义容量的LinkedBlockingQueue )将导致新任务在队列中等待。因此,不会创建超过 corePoolSize 个线程。 (因此,maximumPoolSize 的值没有任何影响。)当每个任务完全独立于其他任务时,这可能是合适的,因此任务不会影响彼此的执行;例如,在网页服务器中。虽然这种排队方式在平滑请求的瞬时突发方面很有用,但它承认当命令平均到达速度快于处理速度时,工作队列可能会无限增长。
3.有界队列。有界队列(例如ArrayBlockingQueue )在与有限的 maximumPoolSizes 一起使用时有助于防止资源耗尽,但可能更难以调整和控制。队列大小和最大池大小可以相互权衡:使用大队列和小池可以最大限度地减少 CPU 使用率、操作系统资源和上下文切换开销,但可能会导致人为地降低吞吐量。如果任务经常阻塞(例如,如果它们受 I/O 限制),系统可能能够为比您允许的更多线程安排时间。使用小队列通常需要更大的池大小,这会使 CPU 更忙,但可能会遇到不可接受的调度开销,这也会降低吞吐量。
被拒绝的任务
当 Executor 关闭时,以及 Executor 对最大线程和工作队列容量都使用有限的界限,并且已经饱和时,在方法execute(Runnable)中提交的新任务将被拒绝。无论哪种情况, execute方法都会调用其RejectedExecutionHandler的RejectedExecutionHandler.rejectedExecution(Runnable,ThreadPoolExecutor)方法。提供了四个预定义的处理程序策略:
1.在默认的ThreadPoolExecutor.AbortPolicy中,处理程序在拒绝时抛出运行时RejectedExecutionException 。
2.在ThreadPoolExecutor.CallerRunsPolicy中,调用execute本身的线程运行任务。这提供了一种简单的反馈控制机制,可以减慢提交新任务的速度。
3.在ThreadPoolExecutor.DiscardPolicy中,无法执行的任务被简单地丢弃。
4.在ThreadPoolExecutor.DiscardOldestPolicy中,如果 executor 没有关闭,则丢弃工作队列头部的任务,然后重试执行(可能再次失败,导致重复此操作。)

可以定义和使用其他类型的RejectedExecutionHandler类。这样做需要小心谨慎,尤其是当策略设计为仅在特定容量或排队策略下工作时。
挂钩方法
此类提供protected的可beforeExecute(Thread, Runnable)和afterExecute(Runnable, Throwable)方法,这些方法在执行每个任务之前和之后调用。这些可用于操纵执行环境;例如,重新初始化 ThreadLocals、收集统计信息或添加日志条目。此外,可以重写方法terminated以执行任何特殊处理,一旦 Executor 完全终止需要完成。
如果钩子或回调方法抛出异常,内部工作线程可能会依次失败并突然终止。
队列维护
方法getQueue()允许访问工作队列以进行监视和调试。强烈建议不要将此方法用于任何其他目的。当大量排队的任务被取消时,提供的两个方法remove(Runnable)和purge可用于协助存储回收。
定稿
程序中不再引用且没有剩余线程的池将自动shutdown 。如果您想确保即使用户忘记调用shutdown也能回收未引用的池,那么您必须通过设置适当的保持活动时间、使用零核心线程的下限和/或设置allowCoreThreadTimeOut(boolean)来安排未使用的线程最终死亡allowCoreThreadTimeOut(boolean) 。
扩展示例。此类的大多数扩展都会覆盖一个或多个受保护的挂钩方法。例如,这是一个添加简单暂停/恢复功能的子类:

class PausableThreadPoolExecutor extends ThreadPoolExecutor {
   private boolean isPaused;
   private ReentrantLock pauseLock = new ReentrantLock();
   private Condition unpaused = pauseLock.newCondition();

   public PausableThreadPoolExecutor(...) { super(...); }

   protected void beforeExecute(Thread t, Runnable r) {
     super.beforeExecute(t, r);
     pauseLock.lock();
     try {
       while (isPaused) unpaused.await();
     } catch (InterruptedException ie) {
       t.interrupt();
     } finally {
       pauseLock.unlock();
     }
   }

   public void pause() {
     pauseLock.lock();
     try {
       isPaused = true;
     } finally {
       pauseLock.unlock();
     }
   }

   public void resume() {
     pauseLock.lock();
     try {
       isPaused = false;
       unpaused.signalAll();
     } finally {
       pauseLock.unlock();
     }
   }
 }

自从:1.5

二、个人总结:

1、核心线程数配置个数:看IO密集型,还是CPU密集型
(1)一般平时cpu使用率4%以下,都是IO密集型,IO密集型核心线程数设置大小具体看实践,目前项目里核心线程数设置50,最大线程数可以和核心线程数相同,队列配置大一些,使永远触发不到最大线程数
(2)如果是大量计算CPU使用率过高,属于CPU密集型,CPU密集型以4C8G为例,核心线程数一般设置4,最大线程数可以和核心线程数相同,队列配置大一些,使永远触发不到最大线程数

2、核心线程数销毁
(1)默认情况下,keep-alive 策略仅适用于超过 corePoolSize 线程的情况,没有任务会进行空跑, 和线程池生命周期一样, 除非线程池shutdown;但是方法allowCoreThreadTimeOut(boolean)也可用于将此超时策略应用于核心线程,只要 keepAliveTime 值不为零即可

版权声明:程序员胖胖胖虎阿 发表于 2022年9月15日 下午5:00。
转载请注明:线程池核心线程数和最大线程数总结 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...