Java 线程池
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
利用 Excutors 创建不同的线程池满足不同场景的需求(五种线程池创建配置)
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,
也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个
新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 - newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直
到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程
因为执行异常而结束,那么线程池会补充一个新线程。 - newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务
所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此
线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大
小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。 - newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性
执行任务的需求。 - newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执
行任务的需求。
Fork/Join 框架(java7 提供的用于并行执行任务的框架)
Fork/Join是 ExecutorService 接口的一种具体实现,目的是为了帮助你更好地利用多处理器带来的好处,它是为了那些能够被递归的拆解成子任务的工作类型量身设计的,其目的在于能够使 用所有可用的运算能力来提升运用的性能。
Fork/Join 把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务
为什么要使用线程池
- 降低资源消耗(即通过重复利用已创建的线程来降低线程创建和销毁造成的消
耗) - 提高线程的可管理性(线程是稀缺资源,如果无限制的创建,不仅会消耗系统
资源,还会降低系统的稳定性,使用线程池也可进行统一的分配,调优和监控)
Executor 框架
是一个根据一组执行策略调用调度执行和控制的异步任务的框架,目的是提供一种将任
务提交与任务如何运行分离开来的机制
J.U.C 的三个 Executor 接口
- Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦
Executor 接口只有一个方法 void execute(Ruunable command)对于不同的 Executor 的实
现,execute 方法可能是创建一个新线程并立即启动,也可能是使用已有的工作线程来
运行传入的任务,也可能是根据设置线程池的容量或者阻塞队列的容量来决定是否要将
传入的线程放入阻塞队列中,或者拒绝接收传入的线程 - ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善
(submit 方法,传入 Callable 接口) - ScheduledExecutorService:(扩展了 ExecutorService,同时,)支持 Future 和定期执
行任务
ThreadPoolExector
描述的是应用从提交任务到线程处去处理,再到线程池内部如何处理任务,再到处理完成返
回数据给应用的流程,线程池会有一个工作队列来接客,即存储用户提交的各个任务,队列
接到任务后,就会排队将任务交给内部的线程池,即工作线程的集合,该集合需要在运行的
过程中管理线程的创建和销毁,例如 newCacheThreadPool,当任务压力较大的时候,线程池
会创建新的工作线程,当任务量变小的时候,线程池会闲置一段时间后结束线程,线程池的
工作线程被抽象为静态内部类 WorkerThreadPool 维护的其实就是一组 Worker 对象
ThreadFactory 提供线程池所需的创建线程的逻辑,如果任务提交被拒绝,比如线程池已经处于 shutdown 的状态,此时先来的任务需要有处理机制来处理,java 标准库中就提供了 ThreadPoolExecutor.AbortPolicy 的默认实现供我们使用之外,还可以按照实际情况扩展自定义相关的处理机制,也就是实现 RejectExecutionHandler 这个接口
ThreadPoolExecutor 构造函数
- corePoolSize: 核心线程数量:(可以大致的理解为长期滞留的线程数,对于不同的线
程池,这个值可能会有很大的区别,核心池的大小,这个参数跟后面讲述的线程池的实现原
理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待
有 任 务 到 来 才 创 建 线 程 去 执 行 任 务 , 除 非 调 用 了 prestartAllCoreThreads() 或 者
prestartCoreThread()方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没
有任务到来之前就创建 corePoolSize 个线程或者一个线程。默认情况下,在创建了线程池后,
线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线
程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中;) - maximumPoolSize: 线程不够用时能够创建的最大线程数
unit:参数 keepAliveTime 的时间单位,有 7 种取值,在 TimeUnit 类中有 7 种静态属性 - workQueue: 任务等待队列(当任务提交时,如果线程池中的线程数量大于或者等于
- corePoolSize 时,把该任务封装成一个 worker 对象,放入到等待队列中,由于存在许多种类
的队列。而使用不同的队列就会有不同的排队机制) - keepAliveTime: 抢占的顺序不一定,看运气(线程池维护线程所允许的空闲时间,当
线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的
线程不会立即被销毁而是会等待,直到等待的时间超过 keepAliveTime,则会终止,直到线
程池中的线程数不超过 corePoolSize,但是如果调用了 allowCoreThreadTimeOut(boolean)方
法,在线程池中的线程数不大于 corePoolSize 时,keepAliveTime 参数也会起作用,直到线程
池中的线程数为 0) - ThreadFactory: 创建新线程,默认使用 Executors.defaultThreadFactory()来创建线程,
使用默认的 ThreadFactory 来创建线程时,会使新创建的线程具有相同的 priority 即优先级,
并且是非守护线程,同时也设置了线程的名称 - Handler: 线程池的饱和策略(如果阻塞队列满了,且没有空闲的线程,这时如果继续
提交任务,就需要采取一种策略处理该任务)
线程池提供了四种策略:
1. AbortPolicy:直接抛出异常,这时默认策略
2. CallerRunsPolicy:用调用者所在的线程来执行任务
3. DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
4. DiscardPolicy:直接丢弃任务
5. 实现 RejectedExecutionHandler 接口的自定义 handler
新任务提交 execute 执行后的判断(4大特性)
流程图:
ThreadPoolExecutor 将状态值和有效线程数合二为一存储到了 AtomicInteger 类型的 ctl 变量里,ctl 是对线程池的运行状态和线程池中有效线程的数量进行控制的字段,主要包括两部分的信息,线程池的运行状态 runState 和线程池内有效线程的数量即 workerCount
runStateOf()获取运行状态,workerCountOf()获取活动线程数,ctlOf 获取两者
线程池的状态:
- RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
- SHUTDOWN:不再接受新提交的任务,但可以处理存量任务(可以处理阻塞队列中以保
存的任务,在线程池处于 RUNNING 状态时,调用 shutdown 方法会使线程池进入到这个状
态里面,在 finalize 方法里执行的过程中也会调用 shutdown 方法使线程池进入到该状态中) - STOP:不爱接受新提交的任务,也不处理存量任务(会中断正在处理任务的线程,在
线程池处于RUNNING或SHUTDOWN状态时,调用shutdwonNow()方法,会使线程池进入STOP
状态) - TIDYING:所有的任务都已终止(正在进行最后的打扫工作,此时 workerCount 即有效的
线程数为 0,线程池进入到这个状态后,在调用 terminated 方法就能进入到 TERMINATED 状
态) - TERMINATED:terminated()方法执行完后进入该状态(默认 terminated 方法里什么都不
做,只是作为一个标识)
状态转换图:
工作线程的生命周期:
线程池的大小如何选定:
- CPU 密集型:线程数 = 按照核数或者核数+1 设定(如果我们的任务主要是进行计算的,
那么就意味着 CPU 的处理能力是稀缺首要的资源,此时大幅度的增加线程来提高计算能力
是不可行的,如果线程太多,可能产生频繁的上下文切换和导致许多不必要的开销) - I/O 密集型:线程数 = CPU 核数*(1+平均等待时间/平均工作时间)(如果是处理较多
等待的任务,例如 IO 操作比较多,)