1.什么是线程池
含义:java中管理线程的工具。
2.线程池是干嘛用的
在java线程创建到完成所消耗的时间一共有三个部分:
1.创建线程的时间--Time1;
2.线程中执行任务的时间--Time2;
3.销毁线程的时间--Time3;
在创建单个线程并运行时,会消耗Time1+Time2+Time3的时常。系统启动一个新线程的成本是比较高的,所以当我们引入多个线程时每次都重新创建线程来运行就显得有些费时了。而线程池正是用来调整或缩短Time1与Time3阶段所消耗的时间所被创造出来的一个管理线程的工具。线程池不仅能调整Time1与Time3花费的时间,还能减少创建线程的数目,提高线程的复用率,从而来提高程序的运行速度。
3.Java提供的线程池及使用
1.单个线程的线程池
ExecutorService pool = Executors.newSingleThreadExecutor();
2.创建指定线程个数的线程池
ExecutorService pool = Executors.newFixedThreadPool(nThread);
nThread:线程的数量。
3.创建可缓存的线程池(自动创建线程,自动回收闲置超过60s的线程)
ExecutorService pool = Executors.newCachedThreadPool();
线程池的使用:
public class Test01 {
public static void main(String[] args) {
//获取的是单个线程的线程池
ExecutorService pool = Executors.newSingleThreadExecutor();
//获取指定数量线程的线程池
//ExecutorService pool = Executors.newFixedThreadPool(3);
//创建可缓存线程的线程池,自动回收60s闲置线程
//ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 1; i <= 100; i++) {
pool.execute(new Task(i));//将任务提交给线程池,线程池去分配线程去处理任务,该方法没有返回值
}
//关闭线程池
pool.shutdown();
}
}
public class Task implements Runnable{
private int i;
public Task(int i) {
this.i = i;
}
@Override
public void run() {
//输出:当前线程池名称 + i(线程执行的输出语句)
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
4.线程池源码部分
ExecutorService pool = Executors.newSingleThreadExecutor();
ExecutorService pool = Executors.newFixedThreadPool(3);
ExecutorService pool = Executors.newCachedThreadPool();
三种线程池底层都是ThreadPoolExecutor类的对象。
ThreadPoolExecutor的构造方法
-- 分析ThreadPoolExecutor类的构造方法源码--------------------------------
public ThreadPoolExecutor(
int corePoolSize, ------------- 核心线程数量
int maximumPoolSize, ------------- 最大线程数量
long keepAliveTime, ------------- 闲置时间,作用于核心线程数与最大线程数之间的线程
TimeUnit unit, ------------- keepAliveTime的时间单位(可以是毫秒、秒....)
BlockingQueue<Runnable> workQueue, -- 任务队列
ThreadFactory threadFactory, -------- 线程工厂
RejectedExecutionHandler handler ---- 拒绝策略
) {}
-- 分析单个线程的线程池的源码 --------------------------------
ExecutorService pool = Executors.newSingleThreadExecutor();
new ThreadPoolExecutor(
1, -- 核心线程数量
1, -- 最大线程数量
0L, -- 闲置时间
TimeUnit.MILLISECONDS, -- 时间单位(毫秒)
new LinkedBlockingQueue<Runnable>() -- 无界任务队列,可以无限添加任务
)
-- 分析指定线程的线程池的源码 --------------------------------
ExecutorService pool = Executors.newFixedThreadPool(3);
new ThreadPoolExecutor(
nThreads, -- 核心线程数量
nThreads, -- 最大线程数量
0L, -- 闲置时间
TimeUnit.MILLISECONDS, -- 时间单位(毫秒)
new LinkedBlockingQueue<Runnable>()-- 无界任务队列,可以无限添加任务
)
-- 创建可缓存线程的线程池 -----------------------------------
ExecutorService pool = Executors.newCachedThreadPool();
new ThreadPoolExecutor(
0, -- 核心线程数量
Integer.MAX_VALUE,-- 最大线程数量
60L, -- 闲置时间
TimeUnit.SECONDS, -- 时间单位(秒)
new SynchronousQueue<Runnable>() -- 直接提交队列(同步队列):没有容量队列
其中
核心线程:可以理解为主要的线程。核心线程一旦被创建不会被自动回收。
最大线程数:该线程池中可同时拥有的最大的线程数量。(最大线程数 - 核心线程数) = 普通线程数
闲置时间:当除了核心线程之外的普通线程闲置一段时间达到闲置时间之后会被回收掉。(不作用于核心线程)
TimeUnit unit:闲置时间的单位,可以是秒,毫秒等。
任务队列:存放任务的队列。当所有核心线程都在处理任务(运行)时,需要处理的任务会被放到任务队列中,等待核心线程的处理。
线程工厂:顾名思义就是用来生成线程的工厂,为了创建具有相似特性的线程所以使用它,例如守护线程等。
拒绝策略:达到了线程界限和队列容量时的处理方案。
线程池的执行步骤:
1.创建线程池后
2.任务提交后,查看是否有核心线程:
3.1 没有 -> 就创建核心线程 -> 执行任务 -> 执行完毕后又回到线程池中
3.2 有 -> 查看是否有闲置核心线程:
4.1 有 -> 执行任务 -> 执行完毕后又回到线程池
4.2 没有 -> 查看当前核心线程数是否核心线程数量:
5.1 否 -> 就创建核心线程 -> 执行任务 -> 执行完毕后又回到线程池中
5.2 是 -> 查看任务列表是否装载满:
6.1 没有 -> 就放入列表中,等待出现闲置线程
6.2 装满 -> 查看是否有普通线程(核心线程数到最大线程数量之间的线程)
7.1 没有 -> 就创建普通线程 -> 执行任务 -> 执行完毕后又回到线程池中
7.2 有 -> 查看是否有闲置普通线程
7.1.1 有 -> 执行任务 -> 执行完毕后又回到线程池中
7.1.2 没有 -> 查看现在所有线程数量是否为最大线程数:
8.1 是 -> 执行处理方案(默认处理抛出异常)
8.2 否 ->就创建普通线程-> 执行任务 -> 执行完毕后又回到线程池中
注:
1.为了更好的理解,在这里区分核心线程和普通线程,实际上区分的没这么清楚,都是线程
2.默认的处理方案就是抛出RejectedExecutionException
总结:核心线程满载 -> 任务队列 -> 普通线程
5.任务队列模式
1.LinkedBlockingQueue无界任务队列
使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。
2.SynchronousQueue 同步任务队列 直接提交任务队列
使用直接提交任务队列,队列没有容量,每执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。 任务队列为SynchronousQueue,创建的线程数大于maximumPoolSize时,直接执行了拒绝策略抛出异常。 使用SynchronousQueue队列,提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的线程,如果达到maximumPoolSize设置的最大值,则根据你设置的handler执行拒绝策略。因此这种方式你提交的任务不会被缓存起来,而是会被马上执行,在这种情况下,你需要对你程序的并发量有个准确的评估,才能设置合适的maximumPoolSize数量,否则很容易就会执行拒绝策略。
3.ArrayBlockingQueue有界任务队列
使用有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列已满时,则会以maximumPoolSize为最大线程数上限。
4.PriorityBlockingQueue优先任务队列
使用优先任务队列,它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize的数量,只不过其他队列一般是按照先进先出的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。
6.拒绝策略
ThreadPoolExecutor自带的拒绝策略有四种,都实现了RejectedExecutionHandler接口
比如:new ThreadPoolExecutor.AbortPolicy()
1.AbortPolicy
当有任务添加到线程池被拒绝时,会抛出RejectedExecutionException异常 线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
2.DiscardPolicy
当有任务添加到线程池被拒绝时,直接丢弃,其他啥都没有。
3.CallerRunsPolicy
当有任务添加到线程池被拒绝时,线程池会将被拒绝的任务添加到线程池正在运行的线程中去运行。 一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。
4.DiscardOledestPolicy
当有任务添加到线程池被拒绝时,线程池会丢弃阻塞队列中末尾的任务(最老的任务--第一个添加的任务),然后将被拒绝的任务添加到末尾。 如果项目中有允许丢失任务的需求,可以使用。