一 最好不要用Java中默认的线程池
好像面试中常考这种题目,简单一句话就是因为容易OOM或因无法创建线程报错。
1.1 CachedThreadPool线程池
在java中定义如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
看下各个参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
阻塞队列用的是SynchronousQueue队列,这是无存储队列,也就是如果没有线程处理它,那就一直处于阻塞状态。根据上次的图,队列处于满的,那就会创建非核心线程,所以newCachedThreadPool
来个任务,如果没有空闲线程,就创建一个线程。这个线程池的最大线程数:maximumPoolSize
定义为Integer.MAX_VALUE所以几乎是无限的,如果任务处理的比较慢,就会创建越来越多的线程,最后导致无法创建线程或oom挂掉。
1.2 FixedThreadPool 线程池
定义如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
固定线程数的线程池,核心线程数和最大线程数的数量是一样的,这个没问题,用的阻塞队列是:
LinkedBlockingQueue
这个队列有个特点,就是无界,可以无限存放任务。如果任务执行的速度比较慢,那就造成在队列中的任务越来越多,最终造成OOM。
1.3 SingleThreadExecutor 线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
单线程池用FinalizableDelegatedExecutorService
包装了下,如下:
static class FinalizableDelegatedExecutorService
extends DelegatedExecutorService {
FinalizableDelegatedExecutorService(ExecutorService executor) {
super(executor);
}
protected void finalize() {
super.shutdown();
}
}
FinalizableDelegatedExecutorService
实现了finalize
,在jvm进行垃圾回收的时候,会调用finalize方法,也就是说我们可以不关闭,虚拟机也会关闭线程池,不过由于这个方法不是一定会调用,为安全期间,还是要主动调用shutdown
等关闭方法来关闭线程池。
继承的DelegatedExecutorService
对一些线程池的方法进行了一层包装,限制了调用了线程池的方法。不具备ThreadPoolExecutor
线程池的所有功能。
1.4 ScheduledThreadPool 线程池
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<Runnable>
,当然正在执行的任务,如果线程中断中未正确处理的话,还是容易丢失任务。
网友评论