JAVA线程池的使用

一、使用 Executors 创建线程池

Executors是一个线程池工厂类,里面有许多静态方法,供开发者调用。

/* 该方法返回一个固定线程数量的线程池,该线程池池中的线程数量始终不变。
 * 当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。
 * 若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务 
 * 默认等待队列长度为Integer.MAX_VALUE
 */
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);

/* 该方法返回一个只有一个线程的线程池。
 * 若多余一个任务被提交到线程池,任务会被保存在一个任务队列中,等待线程空闲,按先入先出顺序执行队列中的任务
 * 默认等待队列长度为Integer.MAX_VALUE
 */
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

/* 
 * 该方法返回一个可根据实际情况调整线程数量的线程池。
 * 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。
 * 若所有线程均在工作,又有新任务的提交,则会创建新的线程处理任务。
 * 所有线程在当前任务执行完毕后,将返回线程池进行复用
 */
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

/* 该方法返回一个ScheduledExecutorService对象,线程池大小为1。
 * ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间内执行某任务的功能,
 * 如在某个固定的延时之后执行,或者周期性执行某个任务
 */
ExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();

/*
 * 该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量
 */
ExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(1);

Executors 的静态方法都是基于 ThreadPoolExecutor 类实现的,相当于 ThreadPoolExecutor 的语法糖。

但这几个静态方法都存在一个弊端,因为会在创建线程池的同时隐式创建等待队列,而队列的长度默认是 Integer.MAX_VALUE ,相当于不限长度,这样就存在OOM的隐患。

二、使用 ThreadPoolExecutor 创建线程池

上面说过,Executors 的静态方法都是基于 ThreadPoolExecutor 类实现的,所以在生产环境下,还是建议直接使用 ThreadPoolExecutor 类创建线程池:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue);

ThreadPoolExecutor 有多个构造方法,一般来说使用最精简的即可。

三、参数含义

corePoolSize

指定线程池的核心线程数。

当一个新任务被添加到线程池时,首先会判断当前的线程数(ThreadCount),如果:

A:ThreadCount < corePoolSize:即当前线程数小于核心线程数,就会创建一个新的线程来执行这个任务;

B:ThreadCount >= corePoolSize:即当前线程数大于等于核心线程数,就会将新任务添加到等待队列中。

该参数的两个特殊参数值:

1、0:意味着没有核心线程,全部线程都会受到 keepAliveTime 参数的回收机制影响。

2、Integer.MAX_VALUE:意味着不限制核心线程数,连等待队列都不需要,可以想象这种情况下很容易OOM。

maximumPoolSize

指定线程池的最大线程数,包括核心线程和非核心线程。

当另一个新任务被添加到线程池时,如果此时等待队列的容量已满,则会判断当前的线程数(ThreadCount),如果:

A:ThreadCount < maximumPoolSize:即当前线程数小于最大线程数,就会创建一个新的线程来执行这个任务;

B:ThreadCount == maximumPoolSize:即当前线程数已达到最大值,此时等待队列的容量也已用尽,因此会抛出异常。

该参数的两个特殊参数值:

1、0:意味着只有核心线程,默认情况下全部线程都不会受到 keepAliveTime 参数的回收机制影响,除非设置 allowCoreThreadTimeOut 为 true。

2、Integer.MAX_VALUE:意味着不限制最大线程数,这种情况下也很容易OOM。

keepAliveTime

空闲线程的存活时间。

默认情况下,该参数只对非核心线程有效。

在处理大量任务时,可能会创建大量的非核心线程,在所有任务都执行完成后会继续保留这些非核心线程一段时间,等时间到了就会自动回收,以减少系统开销。

当设置线程池的 allowCoreThreadTimeOut(true) 时,意味着该参数也同时对核心线程有效,在时间到了之后,全部线程都会自动回收。

unit

空闲线程存活时间的单位。

workQueue

等待队列。

创建线程池时另外一个容易引起OOM的重要参数,主要包括以下几种:

1、ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO(先进先出)排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool() 使用了这个队列。
3、SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
4、PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

 

以最常用的 LinkedBlockingQueue 为例:

//创建一个容量为9999的队列实例
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(9999);

 

关于线程池各参数的作用,可以通过下面的图片进行详细了解:

四、使用线程池的注意事项

一句话:应该最大化的,同时也要有限度的满足业务需求。

在实际使用线程池时,首先应该确保所创建的线程池可以满足业务设计需求,主要就是线程数和队列容量,前者由CPU核心数限制,后者由服务器内存限制。

线程太少,则消费队列的时间就长,就需要更大容量的队列;线程太多,会增加大量的上下文切换时间,反而不利于合理分配CPU的计算资源。

队列太小,则添加任务时可能会抛出异常;队列太大,会占用更多的内存消耗。

关键是切勿使用无边界值(Integer.MAX_VALUE),这也是造成OOM的最主要原因。

可以根据服务器配置和业务需求,对这两个方面进行均衡考虑。

五、使用案例

int cpuCoreCnt = Runtime.getRuntime().availableProcessors(); //获取服务器CPU核心数
int corePoolSize = cpuCoreCnt;      // 核心线程数
int maximumPoolSize = cpuCoreCnt;   // 最大线程数
int keepAliveTime = 30;             // 非核心线程的空闲存活时长(分钟)
int queueCapacity = 9999;           // 队列最大长度

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(queueCapacity);
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, queue);
threadPool.allowCoreThreadTimeOut(true);    //允许回收核心线程

上面案例中,使用CPU核心数作为最大线程数,相对来说还是比较合理的。

等待队列的容量尽可能设置的大一些,和添加任务时抛出异常相比,多付出一些内存来实现更大容量的队列还是非常值得的。

keepAliveTime 也可以适当设置的长一些,避免太快回收,毕竟频繁的创建线程也是需要时间开销的。

最后还设置了allowCoreThreadTimeOut方法,允许自动回收核心线程,用来减少阻塞线程的性能消耗。

六、线程池复用

线程池在完成全部的任务后,会自动进入摸鱼状态,期间会根据配置自动回收空闲线程,直到新的任务被添加进来再起来工作。

即使设置了 allowCoreThreadTimeOut(true) 对核心线程进行回收,有新任务时也会重新创建核心线程继续进入工作状态。

只要不是调用 shutdown() 手动关闭它,正常情况下线程池是可以长期重复性使用的。

有些强迫症患者(比如本人)会非常介意一个无所事事的线程池在内存里装死,因此必须手动 shutdown 才会安心。

但这样的话,之前的线程池就彻底挂掉了,再向其中添加任务时会抛出异常。

有效的做法是,将创建线程池的部分单独封装,每次添加任务时都进行判断,如果当前线程池已经挂掉了,就重新创建一个:

/**
 * <p>
 * 添加任务
 * 注:如果线程池已关闭,会自动创建新的线程池
 * </p>
 * 
 * @param task
 */
public void addTask(Task task){
	if(threadPool.isShutdown()) createThreadPool(corePoolSize, maximumPoolSize, keepAliveTime);
	threadPool.execute(task);
}
页面下部广告