Java线程池详解

如果大家使用过P3C,在使用Executors创建线程时一定看到过这么一句话:
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:
  允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool:
  允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

本文的内容就是对Java线程池相关知识进行比较详细的介绍。

一、Executors

作为Java初学者,我们经常会使用Executors.newXXX来创建线程池,不建议使用的原因上面阿里爸爸已经说明了,我们就一起看看newXXX的源码中进行了什么操作吧。

1、newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                   60L, TimeUnit.SECONDS,
                                   new SynchronousQueue<Runnable>());
 }

2、newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

3、newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

上面三个方法都还有一个ThreadFactory参数的重载,我们可以看到这三个方法其实就是使用一些默认参数为我们新建了ThreadPoolExecutor对象,使用起来更为方便快捷。
接下来就一起看看我们自己使用ThreadPoolExecutor怎么创建线程吧。

二、ThreadPoolExecutor

1. 参数介绍

下面是ThreadPoolExecutor的满参构造函数(删掉了参数值的判断和赋值):

public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue,
                           ThreadFactory threadFactory,
                           RejectedExecutionHandler handler) {
	......
 }

1) corePoolSize
核心线程数,指在线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。

2) maxPoolSize
当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。

3) keepAliveTime
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定。

4) unit
keepAliveTime的计量单位。

5) workQueue
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。
Java中的工作队列可以是BlockingQueue的四种实现:ArrayBlockingQueueLinkedBlockingQueneSynchronousQuenePriorityBlockingQueue

6) threadFactory
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程、ExceptionHandler等等。

7) handler
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:

  1. CallerRunsPolicy:在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
  2. AbortPolicy:直接丢弃任务,并抛出RejectedExecutionException异常。
  3. DiscardPolicy:直接丢弃任务,什么都不做。
  4. DiscardOldestPolicy:抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。

2. 参数设置

上面我们已经介绍了ThreadPoolExecutor构造方法中各个参数的作用,但是各个参数该怎么设置还需要进一步了解。

1) corePoolSize
现在通常是将corePoolSize设置成每秒需要的线程数。
平均每个任务需要花费tasktime秒来处理,则每个线程每秒可以执行1/tasktime个任务。系统每秒有tasks个任务需要处理,则需要的线程数为:tasks/(1/tasktime),即tasks * tasktime个线程数。
假设系统每秒任务数为100 ~ 1000,每个任务耗时0.1秒,则需要100 * 0.1至1000 * 0.1,即10~100个线程。那么corePoolSize应该设置为大于10,具体数字最好根据8020原则,即80%情况下系统每秒任务数,若系统80%的情况下第秒任务数小于200,最多时为1000,则corePoolSize可设置为20。

2) queueCapacity

queueCapacity = (coreSizePool / taskCost) * responseTime

上式中responseTime表示系统对任务的相应时间。如果采用我们上面的例子,假设相应时间设置为2,则队列长度可以设置为:
(corePoolSize/tasktime) * responsetime: (20/0.1)*2 = 400

队列长度设置过大,会导致任务响应时间过长,切忌以下写法:
LinkedBlockingQueue queue = new LinkedBlockingQueue();
这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。

3) maxPoolSize

maxPoolSize = (max(tasks) - queueCapacity) / (1 / taskCost)

当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(1000 - queueCapacity) * (20 / 200),即60个线程,可将maxPoolSize设置为60。

3. 线程池执行步骤

线程池按以下行为执行任务

  1. 当线程数小于核心线程数时,创建线程。
  2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
  3. 当线程数大于等于核心线程数,且任务队列已满:
    • 若线程数小于最大线程数,创建线程
    • 若线程数等于最大线程数,抛出异常,拒绝任务

参考

  • ThreadPoolExecutor线程池参数设置技巧
  • java线程池常用参数设置
  • Java线程池七个参数详解

你可能感兴趣的:(Java)