ThreadPoolExecutor

1. 线程池的状态

在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态

volatile int runState;// 表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性

// 线程池状态
static final int RUNNING  = 0;
// 当创建线程池后,初始时,线程池处于RUNNING状态
static final int SHUTDOWN  = 1;
// 如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
static final int STOP = 2;
// 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
static final int TERMINATED = 3;
// 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态;

2. ThreadPoolExecutor的构造方法

ThreadPoolExecutor是线程的真正实现,通常使用工厂类Executors来创建。

但它的构造方法提供了一系列参数来配置线程池。

ThreadPoolExecutor的构造方法中各个参数的含义:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                          TimeUnit unit, BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);
}

corePoolSize:线程池的核心线程数。

  • 默认情况下,核心线程数会一直在线程池中存活,即使它们处理闲置状态。
  • 如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程在等待新任务到来时会执行超时策略,这个时间间隔由keepAliveTime所指定,
  • 当等待时间超过keepAliveTime所指定的时长后,核心线程就会被终止。

workQueue:线程池中的任务队列,通过线程池的execute方法提交Runnable对象会存储在这个队列中。

  • 当核心线程数达到最大时,新任务会放在队列中排队等待执行。

maximumPoolSize:线程池所能容纳的最大线程数量。

  • 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务。
  • 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常。

keepAliveTime:线程闲置时的超时时长。

  • 超过这个时长,非核心线程就会被回收。直到线程数量=corePoolSize。
  • 当ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true时,keepAliveTime同样会作用于核心线程,直到线程数量=0。

unit:用于指定keepAliveTime参数的时间单位。

  • 这是一个枚举,常用的有TimeUnit.MILLISECONDS(毫秒),TimeUnit.SECONDS(秒)以及TimeUnit.MINUTES(分钟)等。

threadFactory:线程工厂,为线程池提供创建新线程的功能。

  • ThreadFactory是一个接口,它只有一个方法:Thread newThread(Runnable r)。

RejectExecutionHandler,任务拒绝处理器。

  • 表示当ThreadPoolExecutor已经关闭或者ThreadPoolExecutor已经饱和时(达到了最大线程池大小而且工作队列已经满),execute方法将会调用Handler的rejectExecution方法来通知调用者,默认情况下是抛出一个RejectExecutionException异常。

2.1 参数设置

2.1.1 默认值

  • corePoolSize=1
  • queueCapacity=Integer.MAX_VALUE
  • maxPoolSize=Integer.MAX_VALUE
  • keepAliveTime=60s
  • allowCoreThreadTimeout=false
  • rejectedExecutionHandler=AbortPolicy()

2.1.2 如何设置

需要的值:

  • tasks :每秒的任务数,假设为500~1000
  • taskcost:每个任务花费时间,假设为0.1s
  • responsetime:系统允许容忍的最大响应时间,假设为1s

计算:

  • corePoolSize = 每秒需要多少个线程处理? 
    • hreadcount = tasks/(1/taskcost) =tasks*taskcout =  (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
    • 根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可。
  • queueCapacity = (coreSizePool/taskcost)*responsetime
    • 计算可得 queueCapacity = 80/0.1*1 = 800。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
    • 切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
  • maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
    • 计算可得 maxPoolSize = (1000-80)/10 = 92
    • (最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
  • rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
  • keepAliveTime和allowCoreThreadTimeout采用默认通常能满足

以上都是理想值,实际情况下要根据机器性能来决定。如果在未达到最大线程数的情况机器CPU load已经满了,则需要通过升级硬件和优化代码,降低taskcost来处理。

3. 任务提交给线程池之后的处理策略

要知道任务提交给线程池之后的处理策略,主要有以下几点:

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  • 如果当前线程池中的线程数目>=corePoolSize,且任务队列未满,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;
  • 若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
  • 如果当前线程池中的线程数目达到maxinumPoolSize,则会采取任务拒绝策略进行处理;
  • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

4. 任务缓存队列及排队策略

在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务

它决定了缓存任务的排队策略。对于不同的应用场景我们可能会采取不同的排队策略,这就需要不同类型的队列。这个队列需要一个实现了BlockingQueue接口的任务等待队列。

workQueue的类型为BlockingQueue<Runnable>,通常可以取下面类型:

4.1 有限队列 

4.1.1 ArrayBlockingQueue

基于数组的先进先出队列,新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。

此队列创建时必须指定大小。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。

4.1.2 synchronousQueue

这个队列比较特殊,不存储元素的阻塞队列每个插入操作必须等到另一个线程调用移除操作否则插入操作一直处于阻塞状态,吞吐量高于LinkedBlockingQueue,静态工厂方法Exectors.newCachedThreadPool使用了这个队列。

4.2 无限队列

4.2.1 LinkedBlockingQueue

基于链表的先进先出队列;

可以指定容量也可以不指定容量。如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;如果指定了LinkedBlockingQueue的容量大小,那么它反映出来的使用特性就和 ArrayBlockingQueue类似了。

4.2.2 LinkedBlockingDeque

LinkedBlockingDeque是一个基于链表的双端队列。LinkedBlockingQueue的内部结构决定了它只能从队列尾部插入,从队列头部取出元素;但是 LinkedBlockingDeque既可以从尾部插入/取出元素,还可以从头部插入元素/取出元素。

4.2.3 PriorityBlockingQueue

PriorityBlockingQueue是一个按照优先级进行内部元素排序的无限队列。存放在PriorityBlockingQueue中的元素必须实现 Comparable 接口,这样才能通过实现compareTo()方法进行排序。

优先级最高的元素将始终排在队列的头部;

PriorityBlockingQueue不会保证优先级一样的元素的排序,也不保证当前队列中除了优先级最高的元素以外的元素,随时处于正确排序的位置。即,PriorityBlockingQueue并不保证除了队列头部以外的元素排序一定是正确的。

4.2.4 LinkedTransferQueue

LinkedTransferQueue也是一个无限队列,它除了具有一般队列的操作特性外(先进先出),还具有一个阻塞特性:

  • LinkedTransferQueue可以由一对生产者/消费者线程进行操作
  • 当消费者将一个新的元素插入队列后,消费者线程将会一直等待,直到某一个消费者线程将这个元素取走,反之亦然。

5. 任务拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

ThreadPoolExecutor.AbortPolicy丢任务并抛RejectedExecutionException异常。

ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

也可以自定义扩展

以上四种策略均实现了RejectExecutionHandler接口,所以我们自己可以拓展实现该接口

6. 线程池的关闭

ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

7.  线程池容量的动态调整

ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize():

  • setCorePoolSize:设置核心池大小
  • setMaximumPoolSize:设置线程池最大能创建的线程数目大小

 当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。

8. 如何合理配置线程池的大小

调整线程池的大小基本上就是避免两类错误线程太少或线程太多

  • 线程池过小则达不到线程复用的作用,并且会有太多的任务阻塞在缓存队列中。
  • 如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比实际需要的线程可能会引起资源匮乏 问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。

幸运的是,对于大多数应用程序来说,太多和太少之间的余地相当宽。

一般需要根据任务的类型来配置线程池大小

  • 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1。
  • 如果是IO密集型任务,参考值可以设置为2*NCPU;

当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整

9. 为什么使用线程池

无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。

线程池为线程生命周期开销问题和资源不足问题提供了解决方案。

① 通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。

② 因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快

③ 通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足

10. 使用线程池需要注意的地方

① 合理设置线程池的核心数量大小;

② 不要对那些同步等待其他任务结果的任务排队,以免引起死锁;

③ 在为时间可能很长的操作使用合用的线程时要小心,避免阻塞其他线程。

④ 和其他资源一样,线程池在使用完毕后也需要释放,用shutdown()方法可以关闭线程池,如果当时池里还有未被执行的任务,它会等待任务执行完毕,在等待期间试图进入线程池的任务将被拒绝。也可以用shutdownNow()来关闭线程池,它会立刻关闭线程池,没有执行的任务作为返回值返回。

⑤ 执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。

11. 线程池的监控

可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用一下属性:

taskCount:线程池需要执行的任务数量;

completedTaskCount:线程池在运行过程中已完成的任务数量,小于等于taskCount;

largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过;

getPoolSize:线程池的线程数量。

GetActiveCount:获取活动的线程数。

通过扩展线程池进行监控,可以继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法。

12. 线程池中的角色

线程管理器、工作线程、任务接口、任务队列。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值