JDK线程池的总结

1. 概述

1.1 背景/引入

  • 线程是系统资源,每创建新的线程会占用资源,且创建和销毁对象开销较大;
  • 高并发时,如果为每个任务都创建新的线程,则对内存占用太大,可能会内存溢出;
    线程池技术的引入,就是为了解决这一问题.

1.2 线程池工作流程

在这里插入图片描述

当创建线程池,会先创建核心线程数的 核心线程,当任务来了之后就给任务分配线程去执行;
当核心线程被任务占满后,会将任务放到 阻塞队列 排队,等待其他线程执行完毕,再从队列中取出任务来执行;
如果选的是有界队列ArrayBlockingQueue(初始时设置capacity) ,当阻塞队列满了,再来新的任务,就会创建救急线程
当阻塞队列和救急线程都满了,会执行拒绝策略
如果是无解队列LinkedBlockingQueue 则任务可以一直添加到队列,直到内存溢出;

池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利用率。

1.3 作用(为什么要用线程池 ?)

降低资源消耗:通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗,提高线程的重用性。
提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
提高线程的可管理性:线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。

1.4 使用场景

网购商品秒杀,
12306网上购票系统等;
总之
只要有并发的地方、任务数量大或小、每个任务执行时间长或短的都可以使用线程池;
只不过在使用线程池的时候,需要注意设置合理的线程池大小即可;

1.5 类图

在这里插入图片描述
ExecutorService是线程池的基本接口,提供了提交任务、关闭线程池的方法;
ScheduledExecutorSerivce是ExecutorService的子接口,在ExecutorService基础上新增了任务调度的功能,可以定时执行任务;
ThreadPoolExecutor是ExecutorService的基础实现类;
ScheduledThreadPoolExecutor继承于ThreadPoolExecutor, 是带任务调度功能的实现类,并实现了ScheduledExecutorService接口;

2. 线程池状态

在这里插入图片描述

3. ThreadPoolExecutor构造(7大参数)

在这里插入图片描述

3.1 参数说明

在这里插入图片描述
输入5个参数即可使用ThreadPoolExecutor创建线程池:
在这里插入图片描述

阻塞队列:LinkedBlockingQueue listQueue = new LinkedBlockingQueue<Runnable>();

由于构造方法参数太多,初学者不易掌握,所以有Executor工具类提供了工厂方法来创建线程池;

3.2 Executors工厂方法
  1. Executors.newFixedThreadPool(int nThreads)
    创建固定大小的线程池,并直接返回线程池对象;
    源码:底层还是ThreadPoolExecutor
    阻塞队列是LinkedBlockingQueue
    在这里插入图片描述

    在这里插入图片描述
    特点:
    1.参数线程数n是核心线程数,救急线程数为0 ;
    2.阻塞队列是无界的,可以放任意数量的任务(救急线程的前提是选择了有界队列)

  2. Executors.newSingleThreadExecutor():创建单线程线程池
    返回的是装饰器,只有ExecutorService的方法,不能使用ThreadPoolExecutor的方法;
    源码:底层还是ThreadPoolExecutor
    阻塞队列是LinkedBlockingQueue
    在这里插入图片描述
    使用场景:
    希望任务排队串行执行;线程数固定为1,任务数多余1时,会放入无界队列排队

  和自己创建一个任务的区别:
  自己创建的一个线程如果任务失败了没有任何补救措施,而线程池还会创建新的线程,保证线程池的正常工作

  1. Executors.newScheduledThreadPool (int corePoolSize)
    创建一个可用于周期使用的线程池,用于延迟固定间隔时间的任务。
    源码:这次底层用ScheduledThreadPoolExecutor创建的,但是ScheduledThreadPoolExecutorThreadPoolExecutor的子类 !底层调用了super(),即使用父类ThreadPoolExecutor的无参构造!
    阻塞队列是DelayedWorkQueue
    在这里插入图片描述
    在这里插入图片描述
  2. Executors.newCachedThreadPool()
    创建一个可缓存线程池,核心线程数为0;
    每次来任务了,没有新的线程就去创建新的线程来执行;
    线程空闲超过60s就会被销毁
    如果一直创建线程,会导致内存溢出;
    采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理,如果当前没有空闲的线程,那么就会再创建一条新的线程。

  源码:底层还是ThreadPoolExecutor
  阻塞队列是SynchronousQueue
  在这里插入图片描述

为什么不建议使用 Executors静态工厂构建线程池 ?

  1. FixedThreadPool 和 SingleThreadPool使用无界队列,允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM内存溢出;
  2. CachedThreadPool 和 ScheduledThreadPool
    允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
3.3 运行流程

execute() 添加任务到线程池执行,当核心线程数不够时,则将任务放到阻塞队列中排队:
当任务超过了阻塞队列大小时,再来任务,就会创建maximumPoolSize - corePoolSize 个的救急线程来运行任务(前提是选择有界队列);
救急线程的前提是选择了有界队列;若队列是无界队列,即没有容量限制的队列,则不会创建救急线程;
若救急线程也满了则执行拒绝策略;

3.4 救急线程和核心线程的区别 ?

救急线程有生存时间,类似临时工,任务完毕救急线程就被销毁;而核心线程执行完任务依然保留在线程池中;

当线程到达maximumPoolSize最大线程数仍有新任务时,即核心线程和救急线程都被任务占完了,才会执行拒绝策略;

3.5 案例:用工厂方法执行延时、定时执行任务

(1)延时任务
在JDK线程池之前,有 timer 来实现定时功能;
但是Timer的缺点是 所有定时任务都由同一个线程来调度!因此所有任务是 串行 的;同一个时间只有一个人任务执行,前一个任务的延迟或异常都会影响到后面的任务;

所以用 ScheduledThreadPoolExecutor 代替了Timer;
用线程池对象调用 schedule() 方法,参数输入Runnable类型的任务,需要延迟的时间,时间单位;
例:
在这里插入图片描述
此时ScheduledThreadPoolExecutor线程池设置的固定线程个数是2个,所以两个任务能够 并行 执行,而不会串行!
在这里插入图片描述
第一个任务延迟2秒,但因为是并行执行,所以不影响第二个线程的执行;

而当把线程池个数设置为1,且第一个任务出现了异常,依然不会影响第二个线程的执行!

(2)定时任务
即每隔一段时间就会执行任务;使用 scheduledAtFixedRate() 方法即以固定的速率执行;参数为初始延时时间,时间间隔时间,时间单位;
在这里插入图片描述
效果:
在这里插入图片描述

第二种:
在这里插入图片描述
延迟时间在任务结束之后才开始算! 如果任务执行2秒,时间间隔1秒,则实际中间实际为3秒;

4. 阻塞队列

阻塞队列一般用于生产者、消费者场景,生产者往队列中添加元素,消费者从队列取线程;当队列为空,会阻塞消费者;当队列满了,会阻塞生产者;

在线程池中,阻塞队列充当了一个任务的缓冲区,暂时保存没有空闲线程来执行的任务,核心线程空闲时就去阻塞队列中领取任务;一旦队列为空,那么线程就会被阻塞,直到有新任务被插入为止。

阻塞队列有一个非常重要的属性,那就是容量的大小,分为有界无界两种。

4.1 ArrayBlockingQueue

底层是数组
通常,阻塞队列是有界的,如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。(救急线程的前提是选择有界队列 !)
线程安全:加一把锁;

4.2 LinkedBlockingQueue

底层是链表

无界队列意味着里面可以容纳非常多的元素,例如 的上限是 Integer.MAX_VALUE,约为 2 的 31 次方,是非常大的一个数,可以近似认为是无限容量,因为我们几乎无法把这个容量装满。
缺点:无界队列存在比较大的潜在风险,如果在并发量较大的情况下,线程池中可以几乎无限制的添加任务,容易导致内存溢出的问题!

线程安全
用两把锁,同一时刻,可以允许两个线程同时(一个生产者一个消费者)执行!
两把锁分别锁住队列的头部和尾部,
链表头部加一把锁takeLock,即针对所有消费者加锁,保证消费take()的线程安全;
链表尾部加一把锁putLock,即针对所有生产者加锁,保证生产put()的线程安全;
但是生产者和消费者之间是不存在同步性的, 所以效率高!

LinkedBlockingQueue 和 ArrayBlockingQueue 比较:
Linked支持有界,Array强制有界;
底层一个链表、一个数组
Linked两把锁,Array一把锁!ArrayBlockingQueue 性能不如LinkedBlockingQueue ;

4.3 DelayQueue

元素(任务)只有当其指定的延迟时间到了,才能够从队列中获取到该元素,在Executors.newScheduledThreadPoll()中使用;

4.4 SynchronousQueue

一个不存储元素的阻塞队列,类似于无中介的直接交易,每一个put操作必须等待take操作,否则不能添加元素,在Executors.newCachedThreadPool()使用;

它继承了一般的AbstractQueue和实现了BlockingQueue接口。
它与其它的BlockingQueue最大的区别就在它不存储任何数据,它的内部是等待队列用来存储访问SynchronousQueue的线程,而访问它的线程有消费者和生产者,对应于方法put和take。
当一个生产者或者消费者试图访问SynchronousQueue的时候,如果找不到与之能够配对的消费者或者生产者,则当前线程会阻塞,直到对应的线程将其唤醒,或者等待超时,或者中断。

5. JDK线程池的拒绝策略

在这里插入图片描述
AbortPoliocy:抛出异常(默认)
CallerRunsPolicy:让调用线程(提交任务的线程)直接执行此任务
DiscardPolicy:放弃本次任务
DiscardOldestPolicy:放弃最早的任务,当前任务取而代之

6. 提交任务

用创造出来的线程池对象调用以下方法;

  1. execute()
    参数是Runable类型,无返回结果;
    在这里插入图片描述

  2. submit():
    参数是Callable,实现Callable的线程是有返回值的;
    submit方法会返回Future对象,可以用来判断任务是否执行成功;
    FutureTask用来在主线程中接收线程池中返回的结果;调用FutureTask对象的 get() 方法获取返回值;
    在这里插入图片描述

  3. schedule()
    用于 ScheduledThreadPoolExecutor类型的线程池,并设置延时时间,提交任务给线程池去运行;
    在这里插入图片描述

  4. scheduledAtFixedRate()
    用于 ScheduledThreadPoolExecutor类型的线程池,并设置延时时间和 运行时间间隔,提交任务给线程池去运行;在这里插入图片描述例:在这里插入图片描述

  5. invokeAll()
    接收一个集合,执行集合中所有的任务
    返回LIst集合,集合中是Future,
    在这里插入图片描述

  6. invokeAny()
    接受一个集合,不会执行所有任务,而是找到最先执行的任务
    只要集合中有一个任务执行成功,就把整个任务的结果作为返回最终的结果返回,其他的取消;在这里插入图片描述

7. 关闭线程池

  1. shutdown:
    不会接收新的任务,但会处理阻塞队列中剩余任务

  2. shutdownNow:
    将线程池的状态变成stop,即不仅不接受新任务,还会将正在执行的任务停下来 !
    正在执行的任务会用interrupt方法打断 ! 并将任务返回
    在这里插入图片描述

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值