文章目录
一、线程池的好处
线程使应用能够更加充分合理地协调利用CPU、内存、网络、I/O等系统资源。线程的创建需要开辟虚拟机栈、本机方法栈、程序计数器等线程私有的内存空间。在线程销毁时需要回收这些资源。 频繁地创建和销毁线程会浪费大量的系统资源,增加并发编程风险。另外,在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程自身无法解决的。 所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。
1.1 线程池的作用:
- 利用线程池管理并复用线程、控制最大并发数等。
- 实现任务线程队列缓存策略和拒绝机制
- 实现某些与时间相关的功能,如定时执行、周期执行等
- 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池,将较慢的交易与搜索服务隔离开,避免各服务线程相互影响。
1.2 线程是如何创建的
首先从 ThreadPoolExecutor 构造方法分析,如何自定义ThreadFactory 和 RejectedExecutionHandler ,通过分析ThreadExecutor 的execute 和addWorker 两个核心方法,学习如何把任务线程加入到线程池中运行。
ThreadPoolExecutor 的构造方法如下:
public ThreadPoolExecutor(
int corePoolSize, // (第1个参数)
int maximumPoolSize, // (第2个参数)
long keepAliveTime, // (第3个参数)
TimeUnit unit, // (第4个参数)
BlockingQueue<runnable> workQueue, // (第5个参数)
ThreadFactory threadFactory, // (第6个参数)
RejectedExecutionHandler handler) {
// (第7个参数)
if(corePoolSize < 0 ||
// maximumPoolSize 必须大于或等于1 也要大于或等于 corePoolSize (第1处)
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
(第2处)
if (workQueue == null || ThreadFactory == null || handler == null)
throw new NullPointerException();
// 其他代码 ...
}
- 第1个参数: corePoolSize
表示常驻核心线程池数。如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;如果大于 0, 即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。 - 第2个参数:maximumPoolSize
表示线程池能够容纳同时执行的最大线程数。从上方代码中第1处来看,必须大于或等于1。如果maximumPoolSize 与 corePoolSize 相等,即是固定大小线程池。 - 第3个参数:keepAliveTime
表示线程池中的线程空闲时间。当空闲时间达到keepAliveTime 值时,线程会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池的线程数大于 corePoolSize 时,keepAliveTime 才会起作用。但是当ThreadPoolExecutor 的allowCoreThreadTimeOut 变量设置为true 时,核心线程超时后也被回收。 - 第4个参数:TimeUnit
表示时间单位。keepAliveTime 的时间单位通常是TimeUnit.SECONDS 。 - 第5个参数:workQueue
表示缓存队列。当请求的线程数大于corePoolSize 时,线程进入BlockingQueue 阻塞队列,BlockingQueue 队列缓存达到上限后,如果还有新任务需要处理,那么线程池会创建新的线程,最大线程数为 maximumPoolSize 。例如一个生产消费模型队列,使用LinkedBlockingQueue 是单向链表,使用锁来控制入队和出队的原子性,两个锁分别控制元素的添加和获取。 - 第6个参数: threadFactory
表示线程工厂。它用来生成一组相同任务的线程。线程池的命令是通过给这个factory 增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。 - 第7个参数: handler
表示执行拒绝策略的对象。当第5个参数workQueue 的任务缓存区到达上限后,并且活动线程数大于maximumPoolSize 的时候,线程池通过该策略处理请求,这是一种简单的限流保护。像某年双十一没有处理好访问流量过载时的拒绝策略,导致内部测试页面被展示出来,使用户手足无措。友好的拒绝策略可以是如下三种:
(1)保存到数据库进行削峰填谷。在空闲时在提取出来执行。
(2)转向某个提示页面。
(3)打印日志。
从第2处来看,队列、线程工厂、拒绝处理服务都必须有实例对象,但在实际编程中,很少有程序员对这三者进行实例化,而通过Executor 这个线程池静态工厂提供默认实现,那么Executor 与 ThreadPoolExecutor 是什么关系呢?线程池相关类图如下图所示:
/**
* @param 线程任务
* @throws RejectedExecutionException 如果无法创建任何状态的线程任务
*/
void execute (Runnable command);
ExecutorService 接口继承了Executor 接口,定义了管理线程任务的方法。ExecutorService 的抽象类 AbstractExecutorService 提供了submit()、invokeAll() 等部分方法的实现,但是核心方法Executor.execute() 并没有在这里实现。因为所有的任务都在这个方法里执行,不同实现会带来不同的执行策略,这一点在后续的ThreadPoolExecutor 解析时,会进一步分析。通过Executors 的静态工厂方法可以创建三个线程池的包装对象:ForkJoinPool、ThreadPoolExecutor、ScheduledThreadPoolExecutor。
1.3 Executors 分析
Executors 核心的方法由五个:
- Executors.newWorkStealingPool:
JDK8 引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,此构造方法中把CPU 数量设置为默认的并行度:
public statice ExecutorService newWorkStealingPool(){
// 返回 ForkJoinPool (JDK7引入)对象,它也是AbstractExecutorService 的子类
return new ForkJoinPool (Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
}
-
Executors.newCachedThreadPool :
maximumPoolSize 最大可以至Integer.MAX_VALUE , 是高度可伸缩的线程池,如果达到这个上限,相信没有服务器能够继续工作,肯定会抛出OOM 异常。keepAliveTime 默认为60 秒,工作线程处于空闲状态,则回收工作线程。如果认为数增加,再次创建出新线程处理任务。 -
Executors.newScheduledThreadPool:
线程数最大至Integer.MAX_VALUE ,与上述相同,存在OOM 风险。它是ScheduledExecutorService 接口家族的实现类,支持定时及周期性任务执行。相比Timer,ScheduledExecutorService 更安全,功能更强大,与newCachedThreadPool 的区别是不回收工作线程。 -
Executors.newSingleThreadExecutor:
创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行。 -
Executors.newFixedThreadPool :
输入的参数即是固定线程数,既是核心线程数也是最大线程数,不存在空闲线程,所以keepAliveTime 等于 0 :
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlokingQueue<runnable>());
}
这里,输入的队列没有指明长度,下面为LinkedBlockingQueue 的构造方法:
public LinkedBlockingQueue () {
this(Integer.MAX_VALUE);
}
使用这样的无界队列,如果瞬间请求非常大,会有OOM 的风险。除newWorkStealingPool 外,其他四个创建方式都存在资源耗尽的风险。
Executors 中默认的线程工厂和拒绝策略过于简单,通常对用户不够友好。线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生成批号一样,为线程本身指定有意义的名称和相应的序列号。拒绝策略应该考虑到业务场景,返回相应的提示或友好地跳转。