Java线程池分析

线程池,顾名思义,就是一个线程的池子,里面有若干线程,它们的目的就是执行提交给线程池的任务,执行完一个任务后不会退出,而是继续等待或执行新任务。线程池主要由两个概念组成,一个是任务队列,另一个是工作者线程,工作者线程主体就是一个循环,循环从队列中接受任务并执行,任务队列保存待执行的任务。

该文章参考老马的《Java编程的逻辑》

线程池构造方法

线程池有两个构造方法

1
2
3
4
5
6
7
8
9
10
11
12
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

第二个构造方法多了两个参数threadFactory和handler,这两个参数一般不需要,第一个构造方法会设置默认值。
参数corePoolSize, maximumPoolSize, keepAliveTime, unit用于控制线程池中线程的个数,workQueue表示任务队列,threadFactory用于对创建的线程进行一些配置,handler表示任务拒绝策略。下面我们再来详细探讨下这些参数。

线程池参数解析

  • corePoolSize
    核心线程数,当提交任务时如果线程数小于corePoolSize,则直接创建线程执行该任务,否则,将任务添加到阻塞队列

  • maximumPoolSize
    maximumPoolSize表示线程池中的最多线程数,线程的个数会动态变化,但这是最大值,不管有多少任务,都不会创建比这个值大的线程个数。

注:如果阻塞队列采用的是无界队列的话,该参数无意义,因为阻塞队列无界就永远不会满

一般情况下,有新任务到来的时候,如果当前线程个数小于corePoolSiz,就会创建一个新线程来执行该任务,需要说明的是,即使其他线程现在也是空闲的,也会创建新线程。
不过,如果线程个数大于等于corePoolSiz,那就不会立即创建新线程了,它会先尝试排队,需要强调的是,它是”尝试”排队,而不是”阻塞等待”入队,
如果队列满了或其他原因不能立即入队,它就不会排队,而是检查线程个数是否达到了maximumPoolSize,如果没有,就会继续创建线程,直到线程数达到maximumPoolSize。

  • keepAliveTime
    线程空闲时间,空闲时间超过该时间则销毁线程,只对大于corePoolSize~maximumPoolSize的线程有效,即至少保留corePoolSize个线程,即便空闲时间大于keepAliveTime也不销毁。(核心线程也是可以销毁的,需要设置核心线程过期)

注:如果阻塞队列为无界,则maximumPoolSize无意义,那么keepAliveTime也就无意义

  • unit

keepAliveTime的时间单位

  • workQueue
    ThreadPoolExecutor要求的队列类型是阻塞队列BlockingQueue,一般使用LinkedBlockingQueue、SynchronousQueue,用于存放任务,阻塞队列的泛型必须是Runnable。

如果用的是无界队列,需要强调的是,线程个数最多只能达到corePoolSize,到达corePoolSize后,新的任务总会排队,参数maximumPoolSize也就没有意义了。

另一面,对于SynchronousQueue,我们知道,它没有实际存储元素的空间,当尝试排队时,只有正好有空闲线程在等待接受任务时,才会入队成功,否则,总是会创建新线程,直到达到maximumPoolSize。

  • threadFactory
    线程工厂,负责创建线程,指定线程名,线程组,线程优先级,是否为守护线程等信息

  • handler
    拒绝策略,当阻塞队列放不下,且线程数达到最大值maximumPoolSize时,再提交任务,改任务会被拒绝。目前,JDK提供了四种拒绝策略
    CallerRunsPolicy 调用线程执行策略,当前执行的线程执行该任务,可以保证任务不丢失,减缓任务添加的速度
    AbortPolicy 直接抛出异常,会导致线程池抛异常,线程池不可用,默认拒绝策略
    DiscardPolicy 直接丢弃该任务
    DiscardOldestPolicy 丢弃最老的任务,重试添加该任务

注:如果阻塞队列为无界,则拒绝策略无效,因为不会存在任务放不下的情况,也可以自定义自己的拒绝策略。该参数一定要重视

Executors提供的三种线程池

  • FixedThreadPool
    固定大小的线程池,其源代码如下:
1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

通过源码可以看出,线程池的corePoolSize和maximumPoolSize都为指定大小,阻塞队列使用无界阻塞队列(看到无界阻塞队列,就应该想到maximumPoolSize、keepAliveTime、handler都无效),因此,该方法中有用的参数只有corePoolSize和workQueue是有意义的。

存在的问题:当任务执行的较慢,且任务提交的速度过快时,会有大量的任务存放到阻塞队列中,阻塞队列会越来越大,内存会被撑爆,使用该线程池时,一定要考虑清楚。

除了该方法外,Executors还提供了重载方法,可以指定ThreadFactory,但是却没有提供修改阻塞队列的重载方法

使用场景: 负载较重的服务器

  • SingleThreadPool
    单个线程的线程池,与FixedThreadPool相比就是将线程数指定为1,同样该线程池存在FixedThreadPool存在的问题,其源码如下:
    1
    2
    3
    4
    5
    6
    public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()));
    }

与FixedThreadPool类型,Executors也提供了指定ThreadFactory的重载方法

使用场景: 单线程执行环境,保证顺序执行各个任务的场景

  • CachedThreadPool
    使用SynchronousQueue阻塞队列,该队列不保存元素,有任务提交到阻塞队列时,任务必须立即被处理。源码如下:
    1
    2
    3
    4
    5
    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>());
    }

从源码中可以看出,maximumPoolSize的值为Integer.MAX_VALUE,意味着只要有任务到达,且线程池内没有空闲线程,就给任务开辟一个线程去执行。线程空闲60s就销毁

存在问题:如果任务执行时间长,提交速度快,那么会产生大量的线程,引起上下文切换,应用可能会出现假死或者崩溃的情况。

同样,这种类型的线程池,也提供了一个指定ThreadFactory的重载方法
使用场景:适用于大量短期异步任务,或者负载较轻的服务器

由此可见:Executors提供的三种线程池都各自有优缺点,如果使用线程池,建议不要使用这三种线程池,
而是直接通过线程池的构造方法指定自己的corePoolSize,maximumPoolSize,keepAliveTime,阻塞队列workQueue,ThreadFactory,拒绝策略,
自己指定的优点就是可以根据自己的场景灵活的对各个参数进行配置。

线程池提交任务

  • submit()
    提交有返回值的任务,返回值为Future类型(真正的类型是RunnableFuture,而实现RunnableFuture接口的在JDK实现中对外可以使用的就只有FutureTask类

  • execute()
    提交没有返回值的任务

线程池关闭

  • shutdown()
    将线程池的状态修改为shutdown,禁止向线程池中提交任务,并执行完已经提交的任务

  • shutdownNow()
    将线程池的状态修改为stop, 立即终止线程池中的线程, 不处理阻塞队列中的任务,返回没有执行任务的列表

    可以通过isTerminated()方法判断线程池是否完全关闭
    也可以通过awaitTermination(long timeout, TimeUnit unit)最长等待一段时间后退出,但并不能保证关闭