线程池
池化技术
程序的运行,本质:占用系统的资源!,我们需要去优化资源的使用,于是有了 池化技术 例如: 线程池、JDBC的连接池、内存池、对象池 等等 资源的创建、销毁十分消耗资源 池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。
为什么要使用线程池?
Java的线程池是运用场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都可以使用线程池。 合理使用线程池能带来的好处:
- 降低资源消耗。 通过重复利用已经创建的线程降低线程创建的和销毁造成的消耗。例如,工作线程Woker会无线循环获取阻塞队列中的任务来执行。
- 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。 提高线程的可管理性。
- 线程是稀缺资源,Java的线程池可以对线程资源进行统一分配、调优和监控。
1、三大方法
//工具类 Executors 三大方法;
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
//线程池用完必须要关闭线程池
try {
for (int i = 1; i <=100 ; i++) {
//通过线程池创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
源码分析
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
阿里巴巴Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。
2、七大参数
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大的线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂 创建线程的 一般不用动
RejectedExecutionHandler handler //拒绝策略
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
详细说明: 1. corePoolSize(线程池的基本大小):
-
提交一个任务到线程池时,线程池会创建一个新的线程来执行任务。注意: 即使有空闲的基本线程能执行该任务,也会创建新的线程。
-
如果线程池中的线程数已经大于或等于corePoolSize,则不会创建新的线程。 如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
2.maximumPoolSize(线程池的最大数量):
- 线程池允许创建的最大线程数。
- 阻塞队列已满,线程数小于maximumPoolSize便可以创建新的线程执行任务。
- 如果使用无界的阻塞队列,该参数没有什么效果。
3. workQueue(工作队列):
- 用于保存等待执行的任务的阻塞队列。
4.keepAliveTime(线程活动保持时间):
- 线程池的工作线程空闲后,保持存活的时间。如果任务多而且任务的执行时间比较短,可以调大keepAliveTime,提高线程的利用率。
5.unit(线程活动保持时间的单位):
- 可选单位有DAYS、HOURS、MINUTES、毫秒、微秒、纳秒。
6.handler(饱和策略,或者又称拒绝策略): 当队列和线程池都满了,即线程池饱和了,必须采取一种策略处理提交的新任务。
- AbortPolicy: 无法处理新任务时,直接抛出RejectedExecutionException异常,这是默认策略。
- CallerRunsPolicy:用调用者所在的线程来执行任务, 会导致主线程阻塞。
- DiscardOldestPolicy:丢弃阻塞队列中最靠前的一个任务,并执行当前任务。
- DiscardPolicy: 直接丢弃任务。
7. threadFactory: 构建线程的工厂类
3、线程池的五种状态
线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池各个状态切换框架图: 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。
4、线程池工作流程
一个新的任务到线程池时,线程池的处理流程如下:
ThreadPoolExecutor类具体的处理流程:
线程池的核心实现类是ThreadPoolExecutor类,用来执行提交的任务。因此,任务提交到线程池时,具体的处理流程是由ThreadPoolExecutor类的execute()方法去完成的。
- 在创建了线程池后,等待提交过来的任务请求。
- 当调用execute()方法添加一个请求任务时,线程池会做如下判断: 2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务; 2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列; 2.3如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; 2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime) 时,线程池会判断: 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
以银行业务来类比我们线程池
具体业务流程:
- 核心线程数(corePoolSize)(对应我们当值的窗口), 一般请求比较少的时候只有核心线程开着,
- 当核心线程都已被占用了(当值窗口都有人了), 这时新的请求进来, 于是进入我们的阻塞队列(候客区)
- 当请求过多, 阻塞队列也满了 (候客区满是等待的人,开始发牢骚了) , 于是经理打电话叫休假的人来加班, 也就是开启我们其他的窗口, 此时线程开到最大线程数(maximumPoolSize)
- 请求继续增多, 以至于数量超过了最大线程数+ 阻塞队列(所有窗口都有人并且候客区也满了), 这时候经理要在门口维持秩序, 阻止新的客人进来, 也就是启动了拒绝策略
- 当客人们办完事, 陆续开始离场, 于是加班的窗口开始空闲, 但是他们并不会立马走人,而是打起了王者荣耀,看看会不会有多的人过来办理, 当他们打完一局后, 发现没有人来了, 于是准备回家睡觉, 这就是空余线程的存活时间(keepAliveTime), 只有当线程数大于核心线程, 空闲时间超过keepAliveTime, 多余空闲线程会销毁, 直到剩下核心线程数
5.阻塞队列
①特点:
- 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
- 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。
②分类:
①ArrayBlockingQueue:由数组结构组成的有界限塞队列。.
②LinkedBlockingQueue: 由链表结构组成的有界(但大小默认值为Integer.MAX VALUG)阻塞队列(几乎可以认为是无界)
③PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
④DelayQueue:使用优先级队列实现的延迟(按延迟时间排序)无界阻塞队列。
⑤SynchronousQueue: 不存储元素的阻塞队列,也即单个元素的队列。
⑥LinkedTransferQueue:由链表结构组成的无界阻塞队列。
⑦LinkedBlockingDeque:由链表结构组成的双向阻塞队列
③BlockQueue核心方法:
6、四种拒绝策略源码解析
RejectedExecutionHandler接口
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
-
里面只有一个方法。当要创建的线程数量大于线程池的最大线程数的时候,新的任务就会被拒绝,就会调用这个接口里的这个方法。
-
可以自己实现这个接口,实现对这些超出数量的任务的处理。
ThreadPoolExecutor自己已经提供了四个拒绝策略,分别是CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy
这四个拒绝策略其实一看实现方法就知道很简单。
①AbortPolicy
ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常。
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
实现:
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
②CallerRunsPolicy
"调用者运行",该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者去执行 实现:
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
③DiscardPolicy
这个策略的处理就更简单了,看一下实现就明白了:
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
这个东西什么都没干,因此采用这个拒绝策略,会让被线程池拒绝的任务直接抛弃,不会抛异常也不会执行。
④DiscardOldestPolicy
DiscardOldestPolicy策略的作用是,当任务拒绝添加时,会抛弃任务队列中最旧的任务(也就是最先加入队列的任务),再把这个新任务添加进去。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
在rejectedExecution先从任务队列总弹出最先加入的任务,空出一个位置,然后再次执行execute方法把任务加入队列。
⑤自定义拒绝策略
通过看前面的系统提供的四种拒绝策略可以看出,拒绝策略的实现都非常简单。自己写亦一样
比如现在想让被拒绝的任务在一个新的线程中执行,可以这样写:
static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
new Thread(r,"新线程"+new Random().nextInt(10)).start();
}
}
7.如何合理配置线程数
①CPU密集型
- CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
- CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上(悲剧吧),无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:
一般公式: CPU核数+1个线程的线程池
②IO密集型
第一种: 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
第二种:IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力。 所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:
CPU核数 / 1 - 阻塞系数
阻塞系数在0.8~0.9之间
比如8核CPU:
- 8 / 1 - 0.9 = 80个线程数