Skip to content

线程池介绍

一、线程池

线程池就是把一堆线程提前创建好了,放在池子里,当需要用到的时候可以直接使用,使用完以后会把线程放回池子里。

线程池的优势:

  1. 低消耗,降低了创建线程和销毁线程的开销
  2. 提高响应的速度(因为不需要创建线程)
  3. 提高线程的可管理性,可以统一管理线程

线程池的配置

如果线程池中线程的数量过多,最终它们会竞争稀缺的处理器和内存资源,浪费大量的时间在上下文切换上。反之,如果线程的数目过少,正如你的应用所面临的情况,处理器的一些核可能就无法充分利用。线程池大小与处理器的利用率之比可以使用下面的公式进行估算: N(threads) = N(CPU) * U(CPU) * (1 + W/C) 其中: N(CPU)是处理器的核的数目,可以通过Runtime.getRuntime().availableProcessors()得到 U(CPU)是期望的CPU利用率(该值应该介于0和1之间) W/C是等待时间与计算时间的比率

二、线程池状态

状态含义
RUNNING运行状态,该状态下线程池可以接受新的任务,也可以处理阻塞队列中的任务 执行 shutdown 方法可进入 SHUTDOWN 状态 执行 shutdownNow 方法可进入 STOP 状态
SHUTDOWN待关闭状态,不再接受新的任务,继续处理阻塞队列中的任务 当阻塞队列中的任务为空,并且工作线程数为0时,进入 TIDYING 状态
STOP停止状态,不接收新任务,也不处理阻塞队列中的任务,并且会尝试结束执行中的任务 当工作线程数为0时,进入 TIDYING 状态
TIDYING整理状态,此时任务都已经执行完毕,并且也没有工作线程 执行 terminated 方法后进入 TERMINATED 状态
TERMINATED终止状态,此时线程池完全终止了,并完成了所有资源的释放

三、线程池工作原理

当一个任务提交给线程池时。注意:工作队列就有两种实现策略:无界队列和有界队列。无界队列不存在饱和的问题,但是其问题是当请求持续高负载的话,任务会无脑的加入工作队列,那么很可能导致内存等资源溢出或者耗尽。而有界队列不会带来高负载导致的内存耗尽的问题,但是有引发工作队列已满情况下,新提交的任务如何管理的难题,这就是线程池工作队列饱和策略要解决的问题。

  1. 当线程池接收到一个任务时,如果核心线程数没有达到corePoolSize,那么就会新建一个线程,并绑定该任务,直到工作线程的数量达到 corePoolSize 前都不会重用之前的线程。当核心线程数满了的情况,有新的任务进来,如果核心线程有空闲,则使用核心线程执行任务,如果没有空闲,进行第二步
  2. 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步;
  3. 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的临时线程来执行任务,否则,则交给饱和策略进行处理

四、线程池参数

线程池线程数选型,仅参考,实际情况比较复杂

CPU密集型:因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销

IO密集型:IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间。IO密集型表示大部分时间花在等待IO上。

线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程,根据公式:

最佳核心线程数目

CPU密集型 = 核心线程数 = CPU核数 + 1

IO密集型 = CPU核数 / (1-阻塞系数) 例如阻塞系数 0.8,CPU核数为4

  1. corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads(),线程池创建的时候相应的核心线程都会被创建并且启动。

    建议值为:每秒任务数*任务执行时间(例如0.5s) 【100 * 0.2=20】或者cpu数量+1

  2. maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务。

  3. keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。

  4. unit:时间单位。为keepAliveTime指定时间单位。

  5. workQueue:阻塞队列。用于保存任务的阻塞队列,可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue

  6. threadFactory:创建线程的工厂类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。

  7. handler:饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种:

    AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;

    CallerRunsPolicy:只用调用者所在的线程来执行任务;

    DiscardPolicy:不处理直接丢弃掉任务;

    DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务

五、为什么阿里巴巴规范禁止使用Excutors创建线程池

因为Excutors创建线程池也是调用了ThreadPoolExcutor,只是使用了不同的参数、队列、拒绝策略,如果使用不当会造成资源耗尽(OOM问题),不方便排查。根据实际需要创建可以让使用者更加清楚线程池的允许规则等等,规避一些不必要的风险。

常见线程池的问题:

Executors.newFixedThreadPool(0)和Executors.newSingleThreadExecutor()
	队列使用的是LinckedBlockingQueue,队列长度为Integer.MAX_VALUE,当大量请求堆积的时候,容易造成oom
Executors.newCachedThreadPool()和Executors.newScheduledThreadPool(0)
	线程池里允许的最大线程数为Integer.MAX_VALUE,会创建过多线程,造成oom。创建线程时用的内存并不是jvm堆内存,而是系统的剩余内存