[JUC系列]彻底搞懂线程池

使用线程池线程池的目的:

  • 降低资源消耗:创建线程和销毁线程会占用系统资源
  • 提高响应速度:创建线程和销毁线程需要占用时间
  • 方便集中管理:为了防止滥用多线程,有个统一治理的地方

在《阿里巴巴 java 开发手册》中指出线程资源必须通过线程池提供,不允许在应用在显示的创建线程;而且线程池不允许使用 Executors 创建,要通过 ThreadPoolExecutor 方式,由于 jdk 中 Executor 框架虽然提供了如 newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但是还是不够灵活。

1 线程数设置规则

但是根据业务的不同,任务可以分为 IO 密集型和计算密集型,针对不同的类型我们设置的线程数会有不同的规则:
线程池的数量尽量要少,约等于 CPU 的核心数。

  • 针对 IO 密集型:线程池的线程数量要相对较多,约等于 CPU 核心数*2。
  • 针对计算密集型:线程池的数量尽量要少,约等于 CPU 的核心数。

2 线程池原理

java 线程池的实现原理其实很简单,就是一个线程集合 workerSet 和阻塞队列 workQueue。当向线程池提交一个任务的时候,线程池会将任务先放到 workQueue 中,workerSet 中的线程会不断的从 workQueue 中获取线程然后执行。当 workQueue 中没有任务的时候,worker 就会阻塞,直到队列中有任务了就取出来继续执行
在这里插入图片描述

3 线程池流程

当一个任务提交到线程池后,大概的执行流程如下:

  • 线程池首先会判断当前运行的线程的数量是否是小于 corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入到第二步。
  • 判断 BlockingQueue 是否已经满了,如果没有满,就将线程放入 BlockingQueue。否则进入到第三步
  • 创建新的线程直到线程数达到 maximumPoolSize,如果创建一个新的工作线程将使当前运行的线程数量超过 maximumPoolSize,则交给 RejectedExecutionHandler 来处理任务

4 线程池参数

  • corePoolSize:核心线程数,当提交一个任务给线程池时,如果当前线程池的线程数小于 corePoolSize 的话会一直创建新线程执行任务,知道线程数等于 corePoolSize。如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的 prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
  • workQueue:用来保存等待被执行的任务的阻塞队列,在 JDK 中提供了如下阻塞队列
    • ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO 排序任务
    • LinkedBlockingQueue:基于链表结构的阻塞,按 FIFO 排序任务,吞吐量通常要高,在未指明容量的时候,容量默认为 Integer.MAX_VALUE。
    • SynchronousQueue:一个不存元素的阻塞队列,每个插入操作必须等另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue
    • PriorityBlockingQueue:具有优先级的无界阻塞队列
    • DelayQueue:类似于 PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列,要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到取不出来。
  • maximumPoolSize:线程池中允许的最大线程数,如果当前阻塞队列满了,向线程池继续提交任务,如果线程池当前的线程数小于 maximumPoolSize 的值,就会继续创建线程执行任务。当阻塞队列是无界队列的话,则 maximumPoolSize 不起作用,因为无法提交至核心线程池的线程会一直持续放入 workQueue。
  • keepAliveTime:非核心线程空闲时的存活时间,即当线程没有任务执行时,该线程继续存活的时间。
  • unit: keepAliveTime 的单位
  • threadFactory:创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为 DefaultThreadFactory。
  • handler:线程池和饱和策略,当阻塞队列满了,线程池中的线程数大于 maximumPoolSize 并且没有空闲的,如果继续提交任务的话,必须采取一种策略处理这些线程无法处理的任务,线程池提供了四种策略:
    • AbortPolicy:直接抛出异常,默认策略;
    • CallerRunsPolicy:用调用者所在的线程来执行任务;
    • DiscardOldentPolicy:丢弃阻塞队列中最靠前的任务,并执行当前任务;
    • DiscardPolicy:直接丢弃任务。
      我们也可以根据实际的应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

5 Executor 源码解析

FixedThreadPool(定长线程池)

在这里插入图片描述

特点:

  • 使用了 LinkedBlockingQueue,并且没有指定长度

不足:

  • 因为默认值为 Integer.MAX_VALUE,可能会耗费很大的内存,甚至 OOM

ScheduledThreadPool(定时线程池)

在这里插入图片描述

特点:

  • 使用了 LinkedBlockingQueue,并且没有指定长度

不足:

  • 因为默认值为 Integer.MAX_VALUE,可能会耗费很大的内存,甚至 OOM

CacheThreadPool(可缓存线程池)

在这里插入图片描述

特点:

  • maximumPoolSize 的最大值为 Integer.MAX_VALUE,因为其核心线程池数为 0,所以当线程空闲时达到 60s 后都会被回收,极端情况会出现不会持有任何线程资源的情况。

不足:

  • 可能导致创建的线程非常多,甚至 OOM

SingleThreadExecutor(单线程线程池)

在这里插入图片描述

特点:

  • 只有一个核心线程,如果 i 该线程异常结束,则会创建一个新的线程任务来继续执行任务,唯一的线程可以保证所提交的任务的顺序执行,使用了 LinkedBlockingQueue 无界队列

不足:

  • 由于使用了无界队列, 所以 SingleThreadPool 永远不会拒绝, 即饱和策略失效

6 自定义线程池

我们看完源码后发现虽然提供了四种线程池的实现,但是都是有一定的弊端,很多东西不能自由的定义,所以阿里不推荐用 Executor 是有原因的,我们下面来看一下自定义的线程池
在这里插入图片描述

说明:

  • 核心线程数:5
  • 最大线程数:9
  • 非核心线程空闲存活时间:20s
  • 任务队列:长度为 1 的 ArrayBlockingQueue
    在这里插入图片描述

说明:
连续执行 10 次任务,每个任务执行时间为 10s

效果:
首先会使用 5 个核心线程,然后把第六个任务放到任务队列,因为队列的长度为 1,所以后面的任务到达时会判断是否达到了最大线程数,所以第 7~10 个任务会使线程池创建最大线程数到 9,然后过了 10s 后有空闲线程后再执行第六个任务,执行效果如下图。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_41979344/article/details/113483681