并发编程之Executor线程池原理与源码解读
前言
在说线程池之前先了解下基本概念
线程
线程是调度CPU资源的最小单位,线程模型分为KLT模型与ULT模型,JVM使用的KLT模型,Java线程与OS线程保持1:1的映射关系,也就是说有一个java线程也会在操作系统里有 一个对应的线程。
解释下Java线程与OS线程保持1:1的映射关系,如下代码
for (int i=0;i<300;i++){
new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
运行前后对比:发现涨了300
Java线程有多种生命状态
- NEW,新建
- RUNNABLE,运行
- BLOCKED,阻塞
- WAITING,等待
- TIMED_WAITING,超时等待
- TERMINATED,终结
状态切换如下图所示:
yield让出CPU使用权,调用它是轻量级的。
线程池是什么?
上图中运行状态与等待,阻塞,超时等待切换是需要上下文切换的。
会消耗系统资源,还会降低系统的稳定性,因此Java中提供线程池对线程进行统一分配、 调优和监控
池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
线程池介绍
在web开发中,服务器需要接受并处理请求,所以会为一个请求来分配一个线程来进行处理。
如果每次请求都新创建一个线程的话实现起来非常简便,但是存在一个问题:
如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁 线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线程上,花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。
那么有没有一种办法使执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?
这就是线程池的目的了。线程池为线程生命周期的开销和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。
什么时候使用线程池?
- 单个任务处理时间比较短
- 需要处理的任务数量很大
线程池优势
- 重用存在的线程,减少线程创建,消亡的开销,
- 提高性能 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资
源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
ThreadPoolExecutor线程池七大参数解读
- int corePoolSize 核心线程数 :线程数定义了最小可以同时运行的线程数量
- int maximumPoolSize 最大线程数
- long keepAliveTime 最大允许线程不干活时间
- TimeUnit unit 时间单位
- BlockingQueue workQueue :当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
- ThreadFactory threadFactory 线程工厂:为线程池提供创建新线程的线程工厂
- RejectedExecutionHandler handler 拒绝策略:线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
ThreadPoolExecutor 线程池四种拒绝策略
采取一种策略处理该任务,线程池提供了4种策略:
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务; 谁带过来的任务谁执行
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:不处理新任务,直接丢弃掉;
上面的4种策略都是ThreadPoolExecutor的内部类。
线程池流程讲解
- 任务丢到核心线程执行。
- 如果核心线程创建满了,任务丢到阻塞队列里
- 往非核心线程里去放
- 整个线程池已经容不下,再来提交任务,走拒绝策略
当整个线程池一个线程都没有的时候,优先创建的线程就是核心线程。最大线程数=核心线程数+非核心线程数。举个例子结合下面的图
线程池存5种状态解读
1、RUNNING
- 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行 处理。
- 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处 于RUNNING状态,并且线程池中的任务数为0!
2、 SHUTDOWN
- 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
- 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
3、STOP
- 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中 断正在处理的任务。
- 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4、TIDYING
- 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING
状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在
ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;
可以通过重载terminated()函数来实现。
状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也 为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的 任务为空时,就会由STOP -> TIDYING。
5、 TERMINATED
- 状态说明:线程池彻底终止,就变成TERMINATED状态。
- 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING - > TERMINATED。 进入TERMINATED的条件如下: 线程池不是RUNNING状态; 线程池状态不是TIDYING状态或TERMINATED状态; 如果线程池状态是SHUTDOWN并且workerQueue为空; workerCount为0; 设置TIDYING状态成功。
线程数量设置
cpu密集型:cpu数量+1
io密集型:2cpu数量+1
线程池终止
threadPoolExecutor.shutdown(); //running->shutdown
try {
System.out.println("开始时间"+new Date());
boolean a = threadPoolExecutor.awaitTermination(2000, TimeUnit.MILLISECONDS);
System.out.println("结束时间"+new Date());
System.out.println("是否结束:"+a);
} catch (InterruptedException e) {
e.printStackTrace();
}
配合shutdown,若等待时间之内线程池执行完任务,则返回为true。否则为false