Java线程池简单理解
简介
由于主线程的诸多限制,像网络请求等一些耗时的操作我们必须在子线程中 运行。我们往往会通过 new Thread 来开启一个子线程,待子线程操作完成以后通过 Handler 切换到主线程中运行。这么以来我们无法管理我们所创建的子线程,并且无限制的创建子线程,它们相互之间竞争,很有可能由于占用过多资源而导致死机或者内存不足。所以在 Java 中为我们提供了线程池来管理我们所创建的线程。
优势
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或内存不足等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
- 更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单
线程池的创建
有以下三种:
- Executors.newCachedThreadPool():无限线程池。
1 | public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { |
- Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。
1 | public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { |
- Executors.newSingleThreadExecutor():创建单个线程的线程池。
1 | public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { |
线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,规避资源耗尽的风险
- Executors的弊端:
- FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.Max_VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出) - CachedThreadPool和ScheduledThreadPool:
允许的创建线程数目为Integer.Max_VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出)
ThreadPoolExecutor七参数
代码
Executors创建的线程池本质都是基于ThreadPoolExecutor
1 | public ThreadPoolExecutor(int corePoolSize, |
corePoolSize:核心线程数
线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true>后,空闲的核心线程超过存活时间也会被回收)。
大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。
线程池刚创建时,里面没有一个线程,当调用 execute() 方法添加一个任务时,如果正在运行的线程数量小于corePoolSize,则马上创建新线程并运行这个任务。
maximumPoolSize:最大线程数
线程池允许创建的最大线程数量。
当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。
keepAliveTime:空闲线程存活时间
当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。
可被回收的线程:
(1)设置allowCoreThreadTimeout=true的核心线程。
(2)大于核心线程数的线程(非核心线程)。
unit:时间单位
keepAliveTime的时间单位:
1 | TimeUnit.NANOSECONDS |
workQueue:工作队列
新任务被提交后,会先添加到工作队列,任务调度时再从队列中取出任务。工作队列实现了BlockingQueue接口。
JDK默认的工作队列有五种:
(1)ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。
(2)LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无解),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待。
(3)SynchronousQueue 同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素。
(4)PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。
(5)DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出。
threadFactory:线程工厂
创建线程的工厂,可以设定线程名、线程编号等。
1 | public interface ThreadFactory { |
默认采用defaultThreadFactory
1 | public static ThreadFactory defaultThreadFactory() { |
1 | private static class DefaultThreadFactory implements ThreadFactory { |
handler:拒绝策略
当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口。
JDK默认的拒绝策略有四种:
(1)AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
(2)DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
(3)DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
(4)CallerRunsPolicy:由调用线程处理该任务。
基本执行流程
任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。
首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
生命周期
1、RUNNING
2、SHUNDOWN
3、STOP
4、TIDYING
5、TERMINATED
这几个状态的转化关系为:
1、调用shundown()方法线程池的状态由RUNNING——>SHUTDOWN
2、调用shutdowNow()方法线程池的状态由RUNNING——>STOP
3、当任务队列和线程池均为空的时候 线程池的状态由STOP/SHUTDOWN——–>TIDYING
4、当terminated()方法被调用完成之后,线程池的状态由TIDYING———->TERMINATED状态
shutdown()/shutdownNow()有着重要的区别:
shutdown() 执行后停止接受新任务,会把队列的任务执行完毕。
shutdownNow() 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop。
两个方法都会中断线程,用户可自行判断是否需要响应中断。