JDK线程池的总结

导读:本篇文章讲解 JDK线程池的总结,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1. 概述

1.1 背景/引入

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

1.2 什么是线程池

在这里插入图片描述

一个线程集合用来控制线程的数量,会先创建一定数量的线程放入空闲队列中,当任务来了之后就给任务分配线程去执行;
当线程被任务占满后,会将任务放到阻塞队列排队,等待其他线程执行完毕,再从队列中取出任务来执行;
如果选的是有界队列,当阻塞队列满了,再来新的任务,就会创建救急线程;
当救急线程也满了,会执行拒绝策略;

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

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

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

1.4 使用场景

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

1.5 类图

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

2. 线程池状态

在这里插入图片描述

3. ThreadPoolExecutor构造(7大参数)

在这里插入图片描述

参数说明

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

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

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

Executors工厂方法

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

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

  2. Executors.newSingleThreadExecutor():创建单线程线程池
    返回的是装饰器,只有ExecutorService的方法,不能使用ThreadPoolExecutor的方法;
    源码:底层还是ThreadPoolExecutor
    在这里插入图片描述
    使用场景:
    希望任务排队串行执行;线程数固定为1,任务数多余1时,会放入无界队列排队;
    和自己创建一个任务的区别:
    自己创建的一个线程如果任务失败了没有任何补救措施,而线程池还会创建新的线程,保证线程池的正常工作

  3. Executors.newScheduledThreadPool (int corePoolSize)
    创建一个可用于周期使用的线程池,用于延迟和定期任务。
    源码:这次底层用ScheduledThreadPoolExecutor创建的,但是ScheduledThreadPoolExecutor是ThreadPoo的子类l,底层调用了super(),即使用父类ThreadPoolExecutor的无参构造!
    在这里插入图片描述
    在这里插入图片描述

  4. Executors.newCachedThreadPool()
    创建一个可缓存线程池,核心线程数为0,每次来任务了,没有新的线程就去创建新的线程来执行,如果一直创建线程,会导致内存溢出;
    源码:底层还是ThreadPoolExecutor
    在这里插入图片描述

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

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

运行流程

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

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

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

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

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. invokeAll:
    接收一个集合,执行集合中所有的任务
    返回LIst集合,集合中是Future,
    在这里插入图片描述

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

7. 关闭线程池

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/89235.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!