处理器已经进入多核时代,为了充分利用cpu的多核资源,APP应用程序采用多线程并行/并行计算,充分利用多核来提高APP应用程序的性能。 但是,创建线程成本高昂,一方面需要内存资源,另一方面操作系统内核需要将线程添加到调度队列中,开销很大。 这一方面在高并发系统中性能风险非常大,另一方面线程需要消耗内存空间,如果不控制进程创建的线程数,进程的内存空间很可能会耗尽。
为了解决多线程上的两个问题,喜欢听歌的短靴设计了线程池。 线程池有两个主要作用。
1、在不同请求之间重用线程,不需要频繁创建和销毁线程,降低系统开销;
2 .控制线程数量上限,以避免创建过多的线程占用进程内存空间,同时减少线程上下文切换次数。
jdk在java5版本中添加了内置线程池,实现了ThreadPoolExecutor。 本文通过ThreadPoolExecutor的源代码分析线程池在jdk中的实现原理。
线程池由两个核心数据结构组成。
1 ) HashSet,用于存储执行线程集合(workers )任务的线程;
2 )任务等待队列(工作队列) :存储等待线程池调度执行的任务的块队列阻塞队列;
线程池中有几个核心参数。
任务执行过程
任务由execute方法提交到线程池,在提交任务时可能会出现以下情况:
1 )线程池中的线程数少于corePoolSize。 在这种情况下,任务不排队,线程池直接创建线程工作器并执行提交的任务;
2 )如果线程池中的线程数大于或等于corePoolSize,且队列不满,则任务直接添加到队列中,等待线程池的调度执行;
3 )如果线程池中的线程数大于或等于corePoolSize,但队列已满且线程数小于maximumPoolSize,则线程池将扩展,新创建的线程工作器将执行提交任务
4 )如果队列已满,线程数达到maximumPoolSize,则如果线程池无法继续执行任务,任务将被拒绝,指定的拒绝策略将运行。
5 )线程池已关闭。 拒绝任务并运行指定的拒绝策略。
创建线程后,它将继续从队列工作队列中执行任务。 因为工作队列是线程安全的块队列,所以没有线程安全问题。 拉入任务后,运行任务逻辑。 提取任务时有两种情况:
1 )线程池中设置了keepAliveTime参数,此时线程池中的线程数超过核心数corePoolSize,从队列读取任务时将keepAliveTime设置为超时时间超过此时间后,该线程将直接运行run方法主体,而不等待任务,线程将被回收。
2 )否则,线程无限期地等待任务队列,直到任务到来。
拒绝策略(RejectedExecutionHandler )。
当线程集合和等待队列已满时,线程将无法调度任务,线程池将执行默认或用户指定的拒绝策略。
JDK内置的拒绝策略主要包括:
1 )调用“线程执行”(CallerRunsPolicy ),如果任务被线程池拒绝,则任务调用线程执行。
2 )中止执行(AbortPolicy ),如果任务被拒绝,则抛出RejectedExecutionException异常
3 )放弃任务(DiscardPolicy ),直接放弃任务,不抛出异常报告错误;
4 )丢失旧任务(DiscardOldestPolicy ),删除队列中最旧的任务,删除后重新提交当前任务。
除了这些内置拒绝策略外,用户还可以实现RejectedExecutionHandler接口的自定义拒绝策略
关闭线程池
关闭线程池有两个重要步骤。
1 )将线程池的状态更改为关闭。 在这种情况下,新提交到线程池的任务将被直接拒绝。
2 )中断线程池中的所有线程,中断任务执行以回收线程集合中的所有线程。
按计划调度线程池(ScheduledThreadPoolExecutor )。
JDK还包含延迟/定时计划任务的线程池,用于延迟/定时执行已提交的任务。 与常规线程池实现的区别在于,任务队列使用自定义的阻塞队列DelayedWorkQueue,它按时间顺序对添加到队列中的任务进行排序。 从队列中提取任务时,只有在队列标头中的任务超过指定时间时,任务才会出队,否则等待。