我们先来了解了解线程池怎么来的,知其所来,方能知其所往,根据摩尔定律所说:集成电路上可容纳的晶体管数量每 18 个月翻一番,因此 CPU 上的晶体管数量会越来越多。
但随着时间的推移,集成电路上可容纳的晶体管数量已趋向饱和,摩尔定律也渐渐失效,因此多核 CPU 逐渐变为主流,与之相对应的多线程编程也开始变得普及和流行起来,这当然也是很久之前的事了,对于现在而言多线程编程已经成为程序员必备的职业技能了,那接下来我们就来聊聊“线程池”这个多线程编程中最重要的话题,我们先来讲讲线程池的概念,然后说他的优点,那么他有什么用你必然也就知道了。
什么是线程池?
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。池化思想在计算机的应用也比较广泛,比如以下这些:
· 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
· 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
· 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。
线程池的优势主要体现在以下4点:
1. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
4. 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
同时我其实建议大家在idea中添加《阿里巴巴java开发手册》,当然我们也可以下载他电子书看看,其中就写到,线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
知道了什么是线程池以及为什要用线程池之后,我们再来看怎么用线程池
线程池的创建总共有7种,但总体类说可分为2类:
· 一类是通过 ThreadPoolExecutor 创建的线程池;
· 另一个类是通过 Executors 创建的线程池。
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):
1. Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
2. Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
4. Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
5. Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
6. Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
7. ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。(这个是用的最多的,所以参数真有可能问你哦)
ThreadPoolExecutor 参数介绍:
参数 1:corePoolSize
核心线程数,线程池中始终存活的线程数。
参数 2:maximumPoolSize
最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
参数 3:keepAliveTime
最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
参数 4:unit:
单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:
· TimeUnit.DAYS:天
· TimeUnit.HOURS:小时
· TimeUnit.MINUTES:分
· TimeUnit.SECONDS:秒
· TimeUnit.MILLISECONDS:毫秒
· TimeUnit.MICROSECONDS:微妙
· TimeUnit.NANOSECONDS:纳秒
参数 5:workQueue
一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:
· ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
· LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
· SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
· PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
· DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
· LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
· LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。
参数 6:threadFactory
线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。
参数 7:handler
拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
· AbortPolicy:拒绝并抛出异常。
· CallerRunsPolicy:使用当前调用的线程来执行此任务。
· DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
· DiscardPolicy:忽略并抛弃当前任务。
默认策略为 AbortPolicy。
我们讲讲单线程池的意义:
从以上可以看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 创建的都是单线程池,那么单线程池的意义是什么呢?
答:虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。
线程池的执行流程:
ThreadPoolExecutor 关键节点的执行流程如下:
· 当线程数小于核心线程数时,创建线程。
· 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
· 当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。
线程拒绝策略
我们来演示一下 ThreadPoolExecutor 的拒绝策略的触发,我们使用 DiscardPolicy 的拒绝策略,它会忽略并抛弃当前任务的策略
自定义拒绝策略:
除了 Java 自身提供的 4 种拒绝策略之外,我们也可以自定义拒绝策略
你该知道,你该懂的知识都懂了吧,是不是选用哪种线程池你心里有答案了,那我来来说说咱们最后这个问题选用问题:
我们来看下阿里巴巴《Java开发手册》给我们的答案:
【强制要求】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
所以综上情况所述,我们推荐使用 ThreadPoolExecutor 的方式进行线程池的创建,因为这种创建方式更可控,并且更加明确了线程池的运行规则,可以规避一些未知的风险。
最后多说一句,就一句哈哈,ThreadPoolExecutor 的方式进行线程池的创建,ThreadPoolExecutor 最多可以设置 7 个参数,当然设置 5 个参数也可以正常使用,ThreadPoolExecutor 当任务过多(处理不过来)时提供了 4 种拒绝策略,当然我们也可以自定义拒绝策略,这样回答我确定你这道题的面试肯定成功了,并且超长发挥了。
更多关于“java培训”的问题,欢迎咨询千锋教育在线名师。千锋教育多年办学,课程大纲紧跟企业需求,更科学更严谨,每年培养泛IT人才近2万人。不论你是零基础还是想提升,都可以找到适合的班型,千锋教育随时欢迎你来试听。