对于多线程来说,new Thread一定是创建了线程,而Runnable只是一个任务,并没有创建新的线程。 所以,Runnable任务要交给线程来执行。 如果对于每个任务都创建一个线程来执行,显然是不合理的。
线程池就是为了复用线程来处理多个任务,像tomcat, dubbo,数据库连接池等常用的框架都会使用线程池。 在自己开发的过程中,也可能会使用多线程的情况。 比如,一个请求的步骤太多,可以使用多线程并行执行。
线程池继承关系:
看两个核心类:ThreadPoolExecutor 和 ScheduledThreadPoolExecutor
ThreadPoolExecutor:
基本介绍:
java//... private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // 存放任务的阻塞队列 private final BlockingQueue<Runnable> workQueue; // 对线程池内部各种变量进行互斥访问控制 private final ReentrantLock mainLock = new ReentrantLock(); // 线程集合 private final HashSet<Worker> workers = new HashSet<Worker>();
线程池中的每一个线程就是一个 Worker 对象:
javaprivate final class Worker extends AbstractQueuedSynchronizer implements Runnable { // ... final Thread thread; // Worker封装的线程 Runnable firstTask; // Worker接收到的第1个任务 volatile long completedTasks; // Worker执行完毕的任务个数 // ... }
由定义会发现, Worker 继承于 AQS ,也就是说 Worker本身就是一把锁,锁的就是worker对象本身。 lock 方法一旦获取了独占锁,表示当前线程正在执行任务中,那么它会有以下几个作用:
- 如果正在执行任务,则不应该中断线程;
- 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
- 线程池在执行 shutdown 方法或 tryTerminate 方法时,会调用 interruptIdleWorkers 方法来中断空闲的线程,interruptIdleWorkers 中会使用 tryLock 方法来判断线程池中的线程是否是空闲状态。 说简单点,就是不能直接中断在任务中的线程,做到优雅的关闭。
核心参数:
javapublic ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); }
- corePoolSize:在线程池中始终维护的线程个数。
- maxPoolSize:在corePooSize已满、队列也满的情况下,扩充线程至此值。
- keepAliveTime/TimeUnit:maxPoolSize 中的空闲线程存活时间,销毁后线程数缩回corePoolSize。
- blockingQueue:线程池所用的队列类型。
- threadFactory:线程创建工厂,可以自定义,有默认值:Executors.defaultThreadFactory()
- RejectedExecutionHandler:corePoolSize已满,队列已满,maxPoolSize 已满,最后的拒绝策略。
优雅关闭线程池:
当关闭一个线程池的时候,有的线程还正在执行某个任务,有的调用者正在向线程池提交任务,并且队列中可能还有未执行的任务。因此,关闭过程需要一个平滑的过渡,这就涉及线程池的完整生命周期管理。
线程池状态:
线程池五种状态: RUNNING 、 SHUTDOWN 、 STOP 、 TIDYING 和 TERMINATED 。 关闭时,状态值只能从小到大迁移:
在调用 shutdown() 或者 shutdownNow() 之后,线程池并不会立即关闭,接下来需要调用 awaitTermination() 来等待线程池关闭。
awaitTermination中就是不断循环,每隔500ms去校验一下线程池状态是否为TERMINATED,如果是,则返回。 该状态就是上面说的,worker释放锁后就会更新状态。另外,shutdown()只会中断空闲的线程,shutdownNow()会中断所有线程,包括清空任务队列。
线程池执行过程:
- 执行execute方法向池中提交任务,如果核心线程数小于corePoolSize,即使池中还有其他空闲线程,也会创建新线程执行任务。
- 当达到corePoolSize,会放到队列中,空闲线程会从队列中取任务执行,在worker中其实就不断循环的过程。
- 队列已满,则会创建新线程,直到数量达到maxPoolSize,则会执行拒绝策略。
- 空闲时间超过keepAliveTime的线程,会被销毁,直到数量收缩到corePoolSize。 当然,如果线程池设置了allowCoreThreadTimeOut,只要空闲时间超过的线程都会被销毁。
线程池拒绝策略:
在线程池的execute方法的最后,也就是不满足执行条件时,就会执行拒绝策略。 创建线程池默认的handler是RejectedExecutionHandler = new AbortPolicy()。
其实线程池提供了4种:
- AbortPolicy:直接抛异常
- CallerRunsPolicy:直接调用线程任务的run方法执行,线程池不参与(虽然线程池不再接收,但是线程任务可以自己执行)。
- DiscardPolicy:直接丢弃任务,也不报错也不干啥,神不知鬼不觉
- DiscardOldestPolicy:将队列中最早的一条丢弃,再将当前任务添加到队列中。
线程池的队列:
ArrayBlockingQueue
ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
LinkedBlockingQueue
LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,可以一直添加,直到资源耗尽。
DelayQueue
DelayQueue(延迟队列)是一个定时的延迟执行的队列。
SynchronousQueue
SynchronousQueue(直接提交队列),这种队列中不会保存任务,而是直接提交任务给线程处理。如果当前没有线程可用,则新建一个线程处理任务。
Executors:
concurrent 包提供了 Executors 工具类,利用它可以创建各种不同类型的线程池。
单线程的线程池:
固定线程数的线程池:
上面两种都是固定了线程数的,所以核心线程数和最大线程数一样多,因此,使用的队列是无边界的LinkedBlockingQueue,理论上可以一直放任务进去。
每接收一个任务就创建线程来执行,不往队列存放:
单线程具有周期调度功能的线程池:
多线程具有周期调度功能的线程池:
通过以上多种场景下使用的线程池可以发现,各种线程池,无非就是使用不同的参数而已。 Executors还支持传其他参数的线程池,当然,也可以自己定义。
在《阿里巴巴 Java 开发手册》中,其实是明确禁止使用 Executors 创建线程池,并要求开发者自己使用 ThreadPoolExector或 ScheduledThreadPoolExecutor进行创建。这样做是为了强制开发者自己传参,明确线程池的运行策略,使其对线程池的每个配置参数皆做到心中有数,以规避因使用不当而造成资源耗尽的风险。 下面,具体说一下ScheduledThreadPoolExecutor:
ScheduledThreadPoolExecutor:
ScheduledThreadPoolExecutor也是ThreadPoolExector的子类,核心参数都一样,只是做了增强:
延迟执行任务:
有两个方法,可以是Runnable或Callable任务。 在经过delay的时间后,才执行任务。
周期性执行任务:
两个方法:
scheduleAtFixedRate:按固定时间间隔执行,与任务本身的时间没有关系。 比如每隔5s执行一次当前任务。 但是它要求任务本身的执行时间要小于间隔时间,否则下一次开始的时候,上一次还没执行完。
scheduleWithFixedDelay: 也是按固定的时间间隔执行,但是与任务本身的时间有关系。 比如间隔2s执行一次,任务本身的执行时间是10s,那么下一次执行时间就是12s的时候。
- initialDelay:说系统启动后,需要等待多久才开始执行第一次。
- period:间隔执行时间。
延迟执行任务依靠的是 DelayQueue , 是 BlockingQueue的一种,其实现原理是二叉堆。当然这里没有直接使用,而是自己实现了一个DelayedWorkQueue。
而周期性执行任务是执行完一个任务之后,再把该任务扔回到任务队列中,如此就可以对一个任务
反复执行。