好像面试中常考这种题目,简单一句话就是因为容易 OOM 或因无法创建线程报错。
在 java 中定义如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
看下各个参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
阻塞队列用的是 SynchronousQueue 队列,这是无存储队列,也就是如果没有线程处理它,那就一直处于阻塞状态。根据上次的图,队列处于满的,那就会创建非核心线程,所以newCachedThreadPool
来个任务,如果没有空闲线程,就创建一个线程。这个线程池的最大线程数:maximumPoolSize
定义为 Integer.MAX_VALUE 所以几乎是无限的,如果任务处理的比较慢,就会创建越来越多的线程,最后导致无法创建线程或 oom 挂掉。
定义如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
固定线程数的线程池,核心线程数和最大线程数的数量是一样的,这个没问题,用的阻塞队列是:LinkedBlockingQueue
这个队列有个特点,就是无界,可以无限存放任务。如果任务执行的速度比较慢,那就造成在队列中的任务越来越多,最终造成 OOM。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
单线程池用FinalizableDelegatedExecutorService
包装了下,如下:
static class FinalizableDelegatedExecutorService
extends DelegatedExecutorService {
FinalizableDelegatedExecutorService(ExecutorService executor) {
super(executor);
}
protected void finalize() {
super.shutdown();
}
}
FinalizableDelegatedExecutorService
实现了finalize
,在 jvm 进行垃圾回收的时候,会调用 finalize 方法,也就是说我们可以不关闭,虚拟机也会关闭线程池,不过由于这个方法不是一定会调用,为安全期间,还是要主动调用shutdown
等关闭方法来关闭线程池。继承的DelegatedExecutorService
对一些线程池的方法进行了一层包装,限制了调用了线程池的方法。不具备ThreadPoolExecutor
线程池的所有功能。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
用到的队列为DelayedWorkQueue
这种队列有利于延迟执行任务,我们调度任务的时候,可能 5s 后执行任务,也可能 1 个小时后执行任务,这样将 5s 后执行的任务放在队列的前面,这样获取任务比较容易。队列采用堆来实现,这让我想起来大顶堆或小顶堆,用来存放超时或排序的数据挺好的。
这个线程池的问题在于最大线程数:Integer.MAX_VALUE
为无限的,这样如果任务执行的速度比较慢,线程池创建会越来越多,会导致线程创建失败或 OOM 的问题。
newSingleThreadScheduledExecutor
也是一样的问题。
前面说了,java 的默认线程池有一些问题,不建议使用,那么就需要自己设置线程池。我们可以利用ThreadPoolExecutor
来定制属于自己的线程池。
那么最重要的就是两点:1. 我们需要多少个线程;2. 我们用什么阻塞队列比较好。
线程池的线程数量线程池的线程不能太少,如果太少,就难以充分利用 cpu 来提升性能;线程池的线程也不能太多,如果远大于 cpu 的核心数量,那么会有很多线程交叉执行,cpu 需要进行大量的线程切换,这样反而会影响程序的性能。
对于任务来说,如果是 cpu 密集型的任务,如果此主机主要是运行这个程序,可以将线程数设置为主机 cpu 核心数;如果也有其他任务在运行,想性能高点,可以适当扩大些,不建议超过 2 倍的核心数。
对于 IO 密集型任务,线程多数是被 IO 阻塞了,所以可以有更多的线程数,因为如果线程少,这些线程都因为 IO 阻塞的话,那么 cpu 的性能就没有充分利用。有个计算公式:
线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)
阻塞队列除了刚才的线程池,最重要的参数就是阻塞队列了,上文中的几种 Java 线程池默认的队列,都不是太合适,还有一种队列ArrayBlockingQueue
,这个队列采用数组来实现,容量是固定的,如果任务过多,队列会满,线程池的线程数量没有达到最大线程的时候,会继续创建线程;如果线程池的线程数量达到了最大数量,后续的任务会被拒绝,拒绝策略按照前一篇文章,也有可能会丢失,不过这个比用无限队列引起 OOM 要好。
定制线程池的时候,还有创建线程工厂,这个我们可以自己定义一个,比如可以设置创建线程的名字;
拒绝策略拒绝可以参考上一篇文章,有很多种拒绝策略,如果上述的策略不够,我们也可以自己写一个,是将任务持久化,还是直接报错,还是简单的日志记录下,都可以自己封装。
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
自己顶一个拒绝策略类实现这个接口即可。
我们停止线程池,通常的做法是先调用shutdown
,然后调用awaitTermination
等待几秒之后,如果还没有停止则直接调用shutdownNow
方法进行线程池直接关闭。
这种方法现在看来还是不错的,shutdown
调用了,不是直接停止线程池,是需要等待线程池将正在执行的任务和队列中的任务执行完成。这样比较安全,调用shutdown
之后,后续的任务就不再接收了。但是如果我们前台停止的时候,如果队列中一直有任务,那么停止一直停不掉体验不好。
所以调用之后再次调用awaitTermination
代码如下:
ExecutorService fix = Executors.newFixedThreadPool(10);
try {
fix.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
awaitTermination 这个函数,三种情况下会返回:
线程池真正停止了,返回 true。
线程池超过等待时间后,如果还没停止,返回 false。
等待被中断。
shutdownNow
这个比较狠,像kill -9
,但是 java 中也不是一定会杀掉线程,因为shutdownNow
会对线程池中的线程产生中断,如果正在执行的线程不响应中断,忽略中断,那么还会继续停止。另外,此行数是有返回值的,将线程池中队列中未执行的任务汇总到一个 list 中返回,这样可以防止任务的丢失:List
,当然正在执行的任务,如果线程中断中未正确处理的话,还是容易丢失任务。
山有木兮木有枝,心悦君兮君不知。
出自先秦的 [越人歌]
今夕何夕兮,搴舟中流。
今日何日兮,得与王子同舟。
蒙羞被好兮,不訾诟耻。
心几烦而不绝兮,得知王子。
山有木兮木有枝,心悦君兮君不知。