-
newFixedThreadPool中虽然线程数可以限制,但任务数量是无限的,因为使用的是LinkendBlockingQueue无限队列来存储任务,任务不能及时处理,过多堆积,最终OOM;
-
newCachedThreadPool线程数是无上界的,而工作队列是SynchronousQueue没有存储空间的阻塞队列,即有请求到来就创建一个线程处理任务(线程是需要存储空间的),最终OOM无法创建线程;
-
手动创建线程的原因:
-
根据自己的场景、并发情况来评估线程池的几个核心参数,如核心线程数、最大线程数、线程回收策略、工作队列类型以及拒绝策略,确保线程池工作行为符合要求,一般都需要设置有界工作队列和可控线程数
-
任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数暴增、线程死锁、线程占用大量CPU、线程执行出现异常等问题时,往往会抓取线程栈。此时有意义线程名称,有助于定位问题。
-
要用一些监控手段来观察线程池状态。
-
线程池默认工作行为如下:
-
不会初始化corePoolSize个线程,有任务来了才会创建工作线程,即使核心线程有空闲,在没有到核心线程数前,对新任务创建新线程;
-
当核心线程满了之后不会立即扩容线程池,而是把任务堆积到工作队列中;
-
当工作队列满了后扩容线程池,一直到线程个数达到maximumPoolSize为止;
-
如果队列已满且达到了最大线程后还有任务进来,按照拒绝策略处理;
-
当线程数大于核心线程数时,线程等待keepAliveTime后没有任务要处理的话,收缩线程到核心线程数;
-
通过如下手段改变线程池默认工作行为:
-
声明线程池后立即调用prestartAllCoreThreads方法,来启动所有核心线程;
-
传入true给allowCoreThreadTimeOut方法,来让线程池在空闲的时候同样回收核心线程。
-
了解了线程池工作行为,如何设计一个不等任务队列满,而优先创建线程的线程池呢,如下两步:
-
由于线程池在队列满,即无法加入队列情况下才扩容线程池,那么就可以重写队列offer方法,造成队列已满的假象;offer方法是往队尾插入任务,
-
队列已满后,同时达到最大线程数,那么会触发线程池拒绝策略。我们可以自定义拒绝策略,当队列"已满"(假象)后,再真正把任务加入到队列中,通过线程池获取到队列,再通过put加入任务。
-
代码案例
static BlockingQueue queue = new LinkedBlockingQueue(10) {
-
线程池是用来复用的,一个系统根据业务类别用到几个线程池还好,但是要注意使用的时候不要根据请求或其他方法调用来创建线程池,避免造成创建太多线程池。一般就是系统初始化过程,线程池已经创建好,直接使用。
-
注意线程池不要混用,根据程序功能不同,不同策略要配置不同参数的线程池:
-
对于执行慢、数量不大的IO任务,或许要考虑更多的线程数,而不需要太大的队列;
-
-
Linux系统中可以使用wrk压测工具,这款工具可以调用lua脚本,还可以统计吞吐量、延迟、每次请求数据大小等
-
Java8中的parallel stream功能,如IntStream.rangeClosed,可以很方便的并行处理集合中元素,其背后是共享同一个ForkJoinPool,默认并行度是CPU核数-1。对于CPU绑定的任务使用这样的配置比较合适。 如果集合操作设计同步IO操作(数据库操作、外部服务调用等)的话,建议自定义一个ForkJoinPool或普通线程池。
public static void main(String[] args) throws Exception{
ForkJoinPool forkJoinPool=new ForkJoinPool(3);
//注意parallel()方法是将操作分成多个任务,多个任务一起完成这个循环
//如果不使用这个方法,execute中的参数就是一个任务里面做了10次循环
forkJoinPool.execute(()-> IntStream.rangeClosed(1,10).parallel().forEach(i->{
System.out.println(Thread.currentThread().getName());
System.out.println(i);
}));
forkJoinPool.shutdown();
//下面方法是等待在执行shutdown之后,所有任务执行完就终止,否则就等待指定时间后终止
forkJoinPool.awaitTermination(10, TimeUnit.SECONDS);
}