Java线程池种类

前言

线程池的基本思想是一种对象池的思想,开辟一块内存空间,里面存放了众多未死亡的线程,池中线程执行调度由池管理器来处理。当有线程任务时(线程任务保存在队列里),从池中取一个线程,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

JavaExecutors类中提供了6种线程池:

  1. newFixedThreadPool
  2. newCachedThreadPool
  3. newSingleThreadExecutor
  4. newScheduledThreadPool
  5. newSingleThreadScheduledExecutor
  6. newWorkStealingPool

其实这6种线程池都是对线程池类ThreadPoolExecutor的封装,以Executors.newFixedThreadPool()方法为例:

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

下面先介绍线程池类ThreadPoolExecutor,接着按顺序介绍每种线程池的特点,其中第6个很少使用,所以忽略不介绍。

线程池类ThreadPoolExecutor

ThreadPoolExecutor 有多个重载构造函数,下面是它参数最全的一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

下面介绍每个参数的含义。

核心线程数corePoolSize

指核心线程的数量。

“核心线程”、“非核心线程”是一个虚拟的概念,并没有哪些线程被标记为“核心”或“非核心”,这个“核心线程数量”仅仅指线程池中会永远保持几个线程活着不被销毁,即使线程池并没有任务要做。

最大线程数maximumPoolSize

指线程池允许创建的最大线程数。

如果任务队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。所以只有任务队列满了的时候,这个参数才有意义。因此当你使用了无界任务队列的时候,这个参数就没有效果了。

keepAliveTime和TimeUnit

指线程活动保持时间,即当线程池的线程空闲后,保持存活的时间。

所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率,不然线程刚执行完一个任务,还没来得及处理下一个任务,线程就被终止,而需要线程的时候又再次创建,刚创建完不久执行任务后,没多少时间又终止,会导致资源浪费。

注意:这里指的是核心线程池以外的线程。可以设置 allowCoreThreadTimeout = true ,这样就会让核心线程池中的线程也有存活时间的控制。

任务队列workQueue

用来保存“等待被执行的”任务的阻塞队列。即当线程池中无可用线程时,新的任务会被放到此队列中等待被执行。

阻塞队列的含义是,当一个线程从队列中取元素时,队列为空,会要求该线程等待直到队列中有元素可取;当一个线程往队列中添加元素时,队列已满,会要求该线程等待直到队列有位置。所以叫做“阻塞”。非阻塞队列就不会要求线程等待,直接返回操作成功或失败。在 Java 中,阻塞队列就是实现了BlockingQueue接口的类,非阻塞队列就没实现该接口,例如 LinkedList 类。

一般来说可以选择如下阻塞队列:

  1. ArrayBlockingQueue:基于数组的有界阻塞队列
  2. LinkedBlockingQueue:基于链表的阻塞队列
  3. SynchronousQueue:一个不存储元素的阻塞队列
  4. PriorityBlockingQueue:一个具有优先级的阻塞队列

注意:设置阻塞队列时应该指定队列长度,特别是LinkedBlockingQueue,它默认的长度是Integer.MAX_VALUE,会导致队列太长,进而导致OOM。

Integer.MAX_VALUE = 2^31 - 1 = 2147483647 。 Integer.MIN_VALUE = -2^31 = -2147483648

threadFactory

指创建线程的工厂:可以通过线程工厂给每个创建出来的线程设置更加有意义的名字。
默认:DefaultThreadFactory

线程饱和策略RejectedExecutionHandler

指拒绝执行任务的处理,可以理解为饱和策略。当任务队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。
默认:AbortPolicy

  1. AbortPolicy:直接抛出异常 RejectedExecutionException
  2. CallerRunsPolicy: 由调用者所在的线程来运行任务。一般是由 main 线程来执行,会导致 main 线程阻塞
  3. DiscardOldestPolicy:丢弃任务队列里最近的一个任务,并执行当前任务
  4. DiscardPolicy:不处理,直接丢掉

参考资料:https://www.jianshu.com/p/9fec2424de54

ThreadPoolExecutor参数总结

线程池中,固定有corePoolSize数量的存活线程,当它们空闲时,需要被执行的任务直接分配给这些线程执行。当这些线程都不可用时(即繁忙状态),新的任务就加入workQueue任务队列,等待被分配给空闲线程执行。当任务队列满了,线程池开始创建新线程来接收任务,直到线程数量达到maximumPoolSize就不再创建。若任务队列满,且线程数满,则新任务既不能添加到队列中,也不能被线程接收,会根据设置的饱和策略RejectedExecutionHandler处理。

线程池种类特点

newFixedThreadPool

1
2
3
4
5
6
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}

一直拥有固定数量的存活线程的线程池(最大线程数=核心线程数)。

任务阻塞队列是LinkedBlockingQueue(链表阻塞队列),队列长度默认为Integer.MAX_VALUE

若任务队列满了则使用默认的饱和策略AbortPolicy,直接抛出异常 RejectedExecutionException。

newCachedThreadPool

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

核心线程数=0,最大线程数=Integer.MAX_VALUE,即它的线程数无限制。由于无核心线程,每个线程在空闲 60s 后被销毁。

任务阻塞队列是SynchronousQueue,队列长度默认为Integer.MAX_VALUE

SynchronousQueue是一个不存储元素的队列,它只负责传递任务给池中线程,且添加任务时必须等待上一次任务已传递出去。可以理解为有一个任务进来时它就“满了”,需要线程池新建线程来接收队列中的任务。

这个线程池因为线程数无限制,所以一般用来执行任务较小的线程,执行完 60s 后即可销毁。若任务数量多且每个任务执行时间较长,则会不断创建线程,占用内存。

newSingleThreadExecutor

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

一直有且仅有一个线程的线程池(最大线程数=核心线程数=1),线程空闲后即被销毁。

任务阻塞队列是LinkedBlockingQueue(链表阻塞队列),队列长度默认为Integer.MAX_VALUE

若任务队列满了则使用默认的饱和策略AbortPolicy,直接抛出异常 RejectedExecutionException。

这个线程池因为只有一个线程,所以可以保证队列中的任务会被顺序执行(队列本身是先进先出的顺序),若这个线程异常结束,会有另一个取代它。

newScheduledThreadPool

1
2
3
4
5
6
7
8
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, // 纳秒
new DelayedWorkQueue());
}

这个线程池的创建被封装了,其实等同于:

1
2
new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());

核心线程数=corePoolSize,最大线程数=Integer.MAX_VALUE,即它的线程数无限制。线程空闲后即被销毁。

任务阻塞队列是DelayedWorkQueue(优先级队列),队列长度默认为Integer.MAX_VALUE

优先级队列的意思是,它会对插入的数据进行优先级排序,保证优先级越高的数据首先被获取,与数据的插入顺序无关。这里的DelayedWorkQueue就是按照时间排序。

DelayedWorkQueue存储的任务元素只能是RunnableScheduledFutures,而上面提到的 LinkedBlockingQueue、SynchronousQueue 的任务只需要是Runnable

这个线程池提供了schedulescheduleAtFixedRate方法,可以设置任务的延时执行时间和周期性执行时间,用于需要延迟或周期性执行任务的场景。

newSingleThreadScheduledExecutor

1
2
3
4
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}

从创建这个线程池的方法中可以看出,它结合了newSingleThreadExecutornewScheduledThreadPool的特点,只有单线程,任务按优先级队列中的时间顺序执行。

线程池中的线程“活着”的原理

问题:线程池中的线程做完任务后,为什么不会被销毁,而是“活着”等待任务?

ThreadPoolExecutor 借助其成员 Worker worker 间接执行任务线程 thread.run()。Worker 是 ThreadPoolExecutor 的内部类,其中一个属性是Thread thread。同时,worker 本身实现了Runnable,它的 run 方法就是负责执行成员thread.run(),或着等待任务。

当 worker 中的 thread=null 时,会去任务队列中取任务,取到任务则执行任务。取不到任务的情况分两种,一种是活动线程数 < 核心线程数时,worker 会进入阻塞状态,直到能取到任务为止;另一种是活动线程数 > 核心线程数时,worker 会在限定时间内等待任务,若没等到任务,则 worker 线程销毁。

因此,线程池可以保证池中一定有“核心线程数”的线程存在,超过“核心线程数”的线程没有任务就会被销毁。

参考资料: https://blog.csdn.net/anhenzhufeng/article/details/88870374