1.基本概述
线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作
线程池作用:
- 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
- 提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死
- 提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务
池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销
2.操作pool
2.1创建方式
注意:Executor是一个接口,它是线程池框架最顶层的接口,里面有一个方法 execute,ThreadPoolExecutor这个类间接实现了Executor接口,而Executors是一个工具类,它里面的一些方法比如newFixedThreadPool,newCachedThreadPool简化了线程池的创建过程,但是本质还是new 了一个ThreadPoolExecutor。
2.2 创建线程池的7个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数介绍:
corePoolSize:核心线程数,线程池中最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁
maximumPoolSize:最大线程数,线程池能够容纳同时执行的最大线程数,此值大于等于1。一个任务被提交到线程池以后,首先会找有没有空闲并且存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会放到工作队列中,直到工作队列满了,才会创建一个新线程并运行,直到线程数量达到maximunPoolSize。工作队列满,且线程数等于最大线程数,此时再提交任务则会调用拒绝策略。与核心线程数之间的差值又叫救急线程数
keepAliveTime:救急线程最大存活时间,当线程池中的线程数量大于
corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到keepAliveTime时间超过销毁unit:
keepAliveTime参数的时间单位workQueue:阻塞队列,存放被提交但尚未被执行的任务。
threadFactory:线程工厂,创建新线程时用到,可以为线程创建时起名字
handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略
RejectedExecutionHandler 下有 4 个实现类:
- AbortPolicy:让调用者抛出 RejectedExecutionException 异常,默认策略
- CallerRunsPolicy:让调用者运行的调节机制,将某些任务回退到调用者,从而降低新任务的流量
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常
- DiscardOldestPolicy:放弃队列中最早的任务,把当前任务加入队列中尝试再次提交当前任务
补充:其他框架拒绝策略
- Dubbo:在抛出 RejectedExecutionException 异常前记录日志,并 dump 线程栈信息,方便定位问题
- Netty:创建一个新线程来执行任务
- ActiveMQ:带超时等待(60s)尝试放入队列
- PinPoint:它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
3.工作原理
创建线程池,这时没有创建线程(懒惰),等待提交过来的任务请求,调用 execute 方法才会创建线程
当调用 execute() 方法添加一个请求任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
- 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程立刻运行这个任务,对于阻塞队列中的任务不公平。这是因为创建每个 Worker(线程)对象会绑定一个初始任务,启动 Worker 时会优先执行
- 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
当一个线程完成任务时,会从队列中取下一个任务来执行
当一个线程空闲超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小
4.开发要求-怎么确定线程池参数
阿里巴巴 Java 开发手册要求:
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
- 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题
- 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回的线程池对象弊端如下:
- FixedThreadPool 和 SingleThreadPool:请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
- CacheThreadPool 和 ScheduledThreadPool:允许创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致 OOM
创建多大容量的线程池合适?
一般来说池中总线程数是核心池线程数量两倍,确保当核心池有线程停止时,核心池外有线程进入核心池
过小会导致程序不能充分地利用系统资源、容易导致饥饿
过大会导致更多的线程上下文切换,占用更多内存
上下文切换:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换
核心线程数常用公式:
CPU 密集型任务 (N+1): 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数) + 1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 某个核心就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如在内存中对大量数据进行分析
I/O 密集型任务: 这种系统 CPU 处于阻塞状态,用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用,因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N 或 CPU 核数/ (1-阻塞系数),阻塞系数在 0.8~0.9 之间
IO 密集型就是涉及到网络读取,文件读取此类任务 ,特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上
5.execute 和 submit的对比
execute 和 submit 都属于线程池的方法,对比:
execute 只能执行 Runnable 类型的任务,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是封装成 FutureTask,然后调用 execute 执行
execute 会直接抛出任务执行时的异常,submit 会吞掉异常,可通过 Future 的 get 方法将任务执行时的异常重新抛出
6.线程池的线程异常后:销毁还是复用?
当一个线程池里面的线程异常后:
当执行方式是execute时,当执行任务出现异常,内部会抛出异常,进而就会导致线程中断而被移除掉,并且会创建一个新的线程填充到线程池中。
当执行方式是submit时,内部会把异常捕获住且不会抛出,所以就不会把这个线程移除掉,也不会创建新的线程放入到线程池中。
这俩种执行方式,都不会影响线程池里面其他线程的正常执行。 那关于线程池的源码有兴趣可以私信我详细学习, 如果视频有帮助可以三连支持!