公司发的小师妹问我java中的线程池,这么讲可还行?

时间:2021-7-5 作者:qvyue

前言

最近在准备面试资料,就看了一下多线程相关的文章,但是找了很多文章发现大家写的差不多一样,有的文章同样的内容,但是给出的答案却是不一样的,所有我就自己参考一些自己买的书籍和看一下源码总结了一下,也就是以下内容。
这里都是线程池相关的内容,也就是ThreadPoolExecutor这个类相关的内容,至于Executors框架,我会单独再写一篇文章进行详细的写一下,也作为自己的面试总结。

公司发的小师妹问我java中的线程池,这么讲可还行?

一、线程池的实现原理

1、当提交一个任务到线程池时,线程池的处理流程

1、判断核心线程池里的核心线程是否已满

  • 是,判断工作队列是否已经满了
  • 否,不管核心线程有没有空闲,都创建一个新的线程执行这个任务

2、工作队列是否已经满了

  • 已满,判断线程池是否满了
  • 未满,将任务存储在工作队列里

3、判断线程池是否满了【核心线程以外的可以创建的线程,最大线程数】

  • 已满,交给饱和策略handle处理无法执行的任务
  • 未满,创建一个新的线程执行这个任务

以上流程的图解

公司发的小师妹问我java中的线程池,这么讲可还行?
  • 1、 线程池刚创建时是没有线程的,并且里面包含了一个任务队列。

  • 2、 小于核心线程数(corePoolSize),不管有没有空闲的核心线程任务来了都创建一个新的线程;

  • 3、 大于等于核心线程数(corePoolSize)且小于设置的最大线程数(maximumPoolSize),先加入到任务队列(workQueue)里;

  • 4、 队列满了再继续创建线程,直到线程数到达设置的最大线程数(maximumPoolSize);

  • 5、 如果达到最大线程数(maximumPoolSize)且任务队列(workQueue)满了,则通过 handler 所指定的饱和策略来处理此任务;

  • 6、 当线程池中的线程数量大于核心线程数(maximumPoolSize)时,如果某个线程空闲时间超过keepAliveTime,线程将被终止,这样线程池可以动态的调整池中的线程数。

2、ThreadPoolExecutor执行executor()方法的四种情况

  • 1、 如果当前运行的线程少于corePoolSize,则创建新的线程来执行任务【全局锁】

  • 2、 如果运行的线程数大于等于corePoolSize,则将任务加入BlockingQueue阻塞队列

  • 3、 如果BlockingQueue阻塞队列已满,无法加入新的任务,则创建新的线程来处理【全局锁】

  • 4、 如果创建的新的线程促使正在运行的线程数超过了maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandlerrejectedExecution()方法

3、线程池里的线程执行任务分两种情况

  • 1、在executor()方法中创建一个线程,会让这个线程执行当前任务

  • 2、线程在执行完当前任务以后,队列里有任务就会不停的从BlockingQueue中取任务执行。

二、线程池的使用

1、线程池的创建

1.1、new 一个ThreadPoolExecutor调用的构造方法的源码

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

Executor框架中创建的额线程池,都是基于该构造方法创建的。

1.2、ThreadPoolExecutor构造方法的参数解析

1、int corePoolSize

  • 线程池的基本大小

  • 当提交一个线程的时候,线程池会创建一个新的基本线程来执行任务,即使其他空闲的基本线程能够执行新的任务也会创建基本线程

  • 除非需要执行的任务数大于线程池的基本大小,才不会创建新的基本线程

  • 调用线程池的prestartAllCoreThreads()方法线程池会提前创建并启动所有的基本线程

2、int maximumPoolSize

  • 线程池允许创建的最大线程数

  • 如果队列满了,并且已经创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。

  • 无界队列时,这个参数没有意义

3、long keepAliveTime

  • 线程活动保持的时间

  • 线程池的工作线程空闲后,保持存活的时间。

  • 如果任务多,每个任务的执行时间短,可以调大时间,提高线程的利用率

4、TimeUnit unit

  • 线程活动保持的时间的单位

5、BlockingQueue workQueue

  • 用于保持等待执行任务的阻塞队列

6、ThreadFactory threadFactory

  • 用于设置创建线程的工厂
  • 可以通过线程工厂给每个创建出来的线程设置个名字

7、RejectedExecutionHandler handler

  • 饱和策略,就是队列和线程都满了以后的策略
  • 默认的策略是AbortPolicy,表示无法处理新的任务时候抛出【RejectedExecutionException】异常。

1.3、常用的阻塞队列

ArrayBlockingQueue

  • 有界阻塞队列
  • 基于数组结构
  • 按照先进先出【FIFO】原则对元素排序

LinkedBlockingQueue

  • 无界阻塞队列
  • 基于链表结构
  • 按照先进先出【FIFO】原则对元素排序
  • 吞吐量高于ArrayBlockingQueue
  • Executors.newFixedThreadPool()使用的这个队列

SynchronousQueue

  • 不存储元素的阻塞队列
  • 每一个插入操作必须等到另一个线程调用移除操作
  • 没有容量,不存储元素的阻塞队列,也即单个元素的队列,每一个put操作必须要等待一个take操作,否则不能继续添加元素
  • 否则插入操作一直处于阻塞状态
  • 吞吐量高于LinkedBlockingQueue
  • Executors.newCachedThreadPool()使用的这个队列

PriorityBlockingQueue

  • 无界阻塞队列
  • 具有优先级
  • 内部使用数组存储数据,达到容量时,会自动进行扩容,放入的元素会按照优先级进行排序

1.4、四种饱和策略

1、AbortPolicy

  • 该策略是线程池的默认策略。使用该策略时,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。

2、CallerRunsPolicy

  • 只有调用者所在的线程来运行该任务。

  • 谁调用,谁执行。

3、DiscardOldestPolicy

  • 将最早进入队列的任务删除,之后再尝试加入队列。

  • 因为是队列吗,先从任务队列弹出最先加入的任务,空出一个位置,然后再次执行execute方法把任务加入队列。

4、DiscardPolicy

  • 不做任何处理,直接丢掉这个任务,不会有异常抛出。

2、向线程池提交任务

2.1、execute()方法

  • 1、execute()方法用于提交不需要返回值的任务

  • 2、无法判断任务是否被线程池执行成功

2.2、submit()方法

  • 1、submit()方法用于提交需要返回值的任务,会返回一个future类型的对象

  • 2、通过future类型的对象可以判断任务是否执行成功

  • 3、也可以通过future类型的对象的get()方法获取返回值

  • 4、future类型的对象的get()方法会阻塞当前线程知道任务完成

3、自定义一个线程池

 private static final int THREAD_POOL_SIZE = 5;

    public static ThreadPoolExecutor threadPool(){
        // 使用 ThreadFactoryBuilder 创建自定义线程名称的 ThreadFactory,需要引入guava 的maven依赖
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("mine-task-pool-%d").build();

        // 创建线程池,其中任务队列需要结合实际情况设置合理的容量
        return new ThreadPoolExecutor(THREAD_POOL_SIZE,
                THREAD_POOL_SIZE,
                0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue(8),
                namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());
    } 

三、关闭线程池

1、 使用shutdown关闭线程池

  • 将线程池状态置为SHUTDOWN状态,并不会立即停止

  • 内部正在跑的任务和队列里等待的任务,会执行完

  • 中断所有没有正在执行任务的线程,也不会接收新的任务

2、 使用shutdownNow关闭线程池

  • 将线程池状态置为STOP

  • 先停止接收外部提交的任务

  • 尝试停止所有正在执行任务的线程和暂停任务的线程

  • 尝试停止并不是立即停止,尝试将正在跑的任务interrupt中断

  • 返回等待执行任务的列表

3、 两种关闭的原理

  • 遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程。
  • 无法中断响应中断的任务可能永远无法终止。

四、合理的配置线程池

1、 CPU密集型任务

  • CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。
  • 应配置尽可能少的线程,如配置 CPU个数加1个线程的线程池。

2、 IO密集型任务

  • IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O(硬盘/内存) 的读/写操作,此时CPU Loading并不高。

  • 线程并不是一直在执行任务,要配置尽可能多的线程,如2倍CPU个数线程的线程池

3、 混合型任务

  • 拆分为CPU密集型任务和IO密集型任务

4、 一般计算设置的线程数的公式

  • 线程数=cpu核数*(cpu计算时间+io等待时间)/cpu计算时间

五、合理使用线程池带来的三个好处

1、降低资源的消耗

  • 重复利用已经创建的线程,降低线程的创建和销毁带来的消耗

2、提高响应速度

  • 当任务到达的时候,任务不需要等待线程的创建可以直接使用线程池里已经创建的线程

3、提高线程的可管理性

  • 线程池可以进行统一的分配、调优和监控

六、知识脑图

公司发的小师妹问我java中的线程池,这么讲可还行?

写在最后

傻姑粉丝福利:更多一线大厂面试题,高并发等主流技术资料尽在下方
github直达地址:一线大厂面试题,高并发等主流技术资料

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。