一、使用 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); }