线程池
JDK中JUC包多线程内容的讲解,涉及「线程池、阻塞队列、协作器、ThreadLocal、Atomic、Future、锁、CAS、AQS 等」; Object 下wait、notify ;JVM层面 synchronized、volatile 等。 最新文章公众号持续更新中… 欢迎骚扰,分享技术,探讨生活,
线程池简介
线程池,顾名思义是一个放着线程的池子,池子中的线程主要是用来执行任务的。当用户提交任务时,线程池会创建线程去执行任务,若任务超过了核心线程数的时候,会在一个任务队列里进行排队等待(详细往下在讲)。任务,通常是一些大批量的或者耗时的工作单元,会把应用程序的工作分解到多个任务中去执行。一般需要使用多线程执行任务的时候,这些任务最好都是相互独立的,这样有一定的任务边界供程序把控。 多线程,当使用多线程的时候,任务处理过程就可以从主线程中分离出来,任务可以并行处理,同时处理多个请求。任务处理代码必须是线程安全的。
为何要使用线程池?
- 降低开销:在创建和销毁线程的时候会产生很大的系统开销,频繁创建/销毁意味着CPU资源的频繁切换和占用,线程是属于稀缺资源,不可以频繁的创建。假设创建线程的时长记为10ms,线程执行任务的时长记为15ms,销毁线程的时长记为10ms,如果我们执行任务15ms<10ms+10ms,这样的开销是不划算的,不使用线程池去避免创建和销毁的开销,会极大的资源浪费。
- 易复用和管理:将线程都放在一个池子里,便于统一管理(可以延时执行,可以统一命名线程名称(便于排查问题),等),同时,也便于任务进行复用。
- 解耦:将线程的创建和销毁与执行任务完全分离出来,这样方便于我们进行维护,也让我们更专注于业务开发。
线程池的优势
- 提高资源的利用性:通过池化可以重复利用已创建的线程,空闲线程可以处理新提交的任务,从而降低了创建和销毁线程的资源开销。
- 提高线程的管理性:在一个线程池中管理执行任务的线程,对线程可以进行统一的创建、销毁以及监控等,对线程数做控制,防止线程的无限制创建,避免线程数量的急剧上升而导致CPU过度调度等问题,从而更合理的分配和使用内核资源。
- 提高程序的响应性:提交任务后,有空闲线程可以直接去执行任务,无需新建。
- 提高系统的可扩展性:利用线程池可以更好的扩展一些功能,比如定时线程池可以实现系统的定时任务。
线程池记忆口诀
七个参数,四大特性,五个种类、三大工作队列、四大拒绝策略
1.七个参数
一共有7个:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler
int corePoolSize:该线程池中核心线程数最大值
这边我们区分两个概念:
- 核心线程:线程池新建线程的时候,
当前活动的线程总数< corePoolSize
,新建的线程即为核心线程。 - 非核心线程:线程池新建线程的时候,
当前活动的线程总数> corePoolSize
, 且阻塞队列已满,这时新建一个线程来执行新提交的任务即为非核心线程。
核心线程默认情况下会一直存活在线程池中,即使这个核心线程不工作(空闲状态),除非ThreadPoolExecutor 的 allowCoreThreadTimeOut
这个属性为 true
,那么核心线程如果空闲状态下,超过一定时间后就被销毁。
int maximumPoolSize:线程总数最大值
线程总数 = 核心线程数 + 非核心线程数
long keepAliveTime:非核心线程空闲超时时间
keepAliveTime即为空闲线程允许的最大的存活时间。如果一个非核心线程空闲状态的时长超过keepAliveTime了,就会被销毁掉。注意:如果设置allowCoreThreadTimeOut = true
,就变成核心线程超时销毁了。
TimeUnit unit:是keepAliveTime 的单位
TimeUnit 是一个枚举类型,列举如下:
2.四大特性
1.当前线程数量未达到 corePoolSize
,则新建一个线程(核心线程)
执行任务
2.当前线程数量达到了 corePoolSize
,则将任务移入阻塞队列等待
,让空闲线程处理;
3.当阻塞队列已满
,新建线程(非核心线程)
执行任务
4.当阻塞队列已满,总线程数又达到了 maximumPoolSize
,就会按照拒绝策略处理无法执行的任务,比如RejectedExecutionHandler抛出异常。
3.五个种类
- 常用四种
创建固定数目线程的线程池 Executors.newFixedThreadPool(200);
创建一个无限线程的线程池,无需等待队列,任务提交即执行 Executors.newCachedThreadPool()
创建有且仅有一个线程的线程池Executors.newSingleThreadExecutor();
创建一个定时周期执行的线程池Executors.newScheduledThreadPool();
注意事项
1.FixedTheradPool和SingleThreadedPool因为是固定core和max,所以其阻塞任务队列是LinkedBlockingQueue没有边界的所以任务过多处理不完会导致OOM
2.CachedThreadPool和S cheduledThreadPool因为max是Integer.MAX_VALUE,阻塞队列是固定长度的ArrayBlockingQueue所以在队列满之后一直创建新线程可能会导致OOM
PS: 阿里禁止使用Executors线程池工具创建线程池,要使用ThreadPoolExecutor根据构造函数应用场景硬件等自定义线程池运行规则
4.三大工作队列
1.无界队列 LinkedBlockingQueue
2.有界队列 ArrayBlockingQueue
3.直接交界 SynchronousQueue
5.四大拒绝策略
ThreadPoolExecutor的拒绝策略可以通过调用setRejectedExecutionHandler
来修改。JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。 拒绝策略如下:
RejectedExecutionHandler rejected = null;
//默认策略,阻塞队列满,则丢任务、抛出异常
rejected = new ThreadPoolExecutor.AbortPolicy();
//阻塞队列满,则丢任务,不抛异常
rejected = new ThreadPoolExecutor.DiscardPolicy();
//删除队列中最旧的任务(最早进入队列的任务),尝试重新提交新的任务
rejected = new ThreadPoolExecutor.DiscardOldestPolicy();
//队列满,不丢任务,不抛异常,若添加到线程池失败,那么主线程会自己去执行该任务
rejected = new ThreadPoolExecutor.CallerRunsPolicy();
(1)AbortPolicy、DiscardPolicy和DiscardOldestPolicy AbortPolicy
是默认的饱和策略
,就是中止任务,该策略将抛出RejectedExecutionException。调用者可以捕获这个异常然后去编写代码处理异常。 当新提交的任务无法保存到队列
中等待执行时,DiscardPolicy
会悄悄的抛弃该任务
。DiscardOldestPolicy
则会抛弃最旧的
(下一个将被执行的任务),然后尝试重新提交新的任务。如果工作队列是那个优先级队列时,搭配DiscardOldestPolicy饱和策略会导致优先级最高的那个任务被抛弃,所以两者不要组合使用。
(2)CallerRunsPolicy CallerRunsPolicy是“调用者运行”策略,实现了一种调节机制 。它不会抛弃任务
,也不会抛出异常
。 而是将任务回退到调用者
。它不会在线程池中
执行任务,而是在一个调用了execute的线程中
执行该任务。在线程满后,新任务将交由调用线程池execute方法的主线程执行,而由于主线程在忙碌,所以不会执行accept方法,从而实现了一种平缓的性能降低。 当工作队列被填满后,没有预定义的饱和策略来阻塞execute(除了抛弃就是中止还有去让调用者去执行)。然而可以通过Semaphore来限制任务的到达率。
线程池运行状态
RUNNING:运行状态,指可以接受任务并执行队列里的任务。
SHUTDOWN:调用了 shutdown() 方法,不再接受新任务,但队列里的任务会执行完毕。
STOP:指调用了 shutdownNow() 方法,不再接受新任务,所有任务都变成STOP状态,不管是否正在执行。该操作会抛弃阻塞队列里的所有任务并中断所有正在执行任务。
TIDYING:所有任务都执行完毕,程序调用 shutdown()/shutdownNow() 方法都会将线程更新为此状态,若调用shutdown(),则等执行任务全部结束,队列即为空,变成TIDYING状态;调用shutdownNow()方法后,队列任务清空且正在执行的任务中断后,更新为TIDYING状态。
TERMINATED:终止状态,当线程执行 terminated()
后会更新为这个状态。
线程池体系结构
顶层接口Executor只有一个方法executor
核心源码分析(有没有像上边执行任务的四个规则)
runwork简单梳理【runwork 线程复用原理: getTask从阻塞队列拿任务 task.run执行while循环 work不会停止一直在取任务,并且执行任务会加锁;前beforeExecuter 后afterExecute 钩子函数控制线程池中线程执行情况,并且能够对线程执行前/后进行处理】
Execute源码详细分析推荐 https://juejin.cn/post/6844904070906380301
ExecutorService线程池管理
ExecutorService继承Executor有几个Executor没有的几个方法
比如:shutdown 、shutdownNow、 isshutdown、 isTerminated、 awaitTermination 、 subimit 、 invokall 等初步管理线程池方法
//关闭线程池,但阻塞队列中的任务继续执行完之后才关闭
void shutdown();
//关闭线程池,阻塞队列中的任务直接丢弃
List<Runnable> shutdownNow();
//是否处于SHUTDOWN状态
boolean isShutdown();
//是否处于TERMINATED状态
boolean isTerminated();
//等待线程池进入TERMINATED状态
boolean awaitTermination( long timeout, TimeUnit unit)
throws InterruptedException;
//提交任务,不过任务有返回值
<T> Future<T> submit(Callable<T> task);
//提交任务,不过任务有返回值
<T> Future<T> submit(Runnable task, T result);
//提交Runnable任务,会自动封装成Callable任务
Future<?> submit(Runnable task);
AbstractExecutorService线程池管理
这个类是一个抽象类,实现了ExecutorService,主要是为该接口的方法提供一些默认的实现。
ThreadPoolExecutor创建线程池核心方法
这个类就是线程池实现的核心类,线程池的线程管理和阻塞队列管理都是它完成的
线程数设定规则
**CPU 密集型任务(N+1):**比如圆周率计算
这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
**I/O 密集型任务(2N):**比如大文件耗时处理
这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。