java线程池知识点

文章目录
  1. 1. 为什么用线程池,优势
  2. 2. 线程池如何使用
    1. 2.1. 架构说明
    2. 2.2. 编码实现
    3. 2.3. 阿里巴巴 java 开发规范
  3. 3. 线程池的几个重要参数介绍
    1. 3.1. corePoolSize
    2. 3.2. maximumPoolSize
    3. 3.3. keepAliveTime
    4. 3.4. unit
    5. 3.5. workQueue
    6. 3.6. threadFactory
    7. 3.7. handler
  4. 4. 线程池的底层工作原理?
  5. 5. 线程池的拒绝策略
    1. 5.1. 是什么
    2. 5.2. JDK 内置的拒绝策略
  6. 6. 合理配置线程池你是如何考虑的?
    1. 6.1. CPU密集型
    2. 6.2. IO密集型

为什么用线程池,优势

线程池主要是控制运行线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

主要特点是:线程复用、控制最大并发数、管理线程。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池如何使用

架构说明

Java中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类。

编码实现

  1. Executors.newFixedThreadPool(int)

    执行长期的任务,性能好很多

    主要特点:

    1. 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    2. newFixedThreadPool 创建的线程池 corePoolSize 和 maximumPoolSize 值是相等的,它使用的 LinkedBlockingQueue。
  2. Executors.newSingleThreadExecutor()

    一个任务一个任务执行的场景

    主要特点:

    1. 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
    2. newSingleThread 将 corePoolSize 和 maximumPoolSize 都设置为1,它使用的是LinkedBlockingQueue。
  3. Executors.newCachedThreadPool()

    适用:执行很多短期异步的小程序或者负载较轻的服务器

    主要特点:

    1. 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    2. newCachedThreadPool 将 corePoolSize 设置为 0,将 maximumPoolSize 设置为 Integer.MAX_VALUE,使用的 SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过 60 秒,就销毁线程。

阿里巴巴 java 开发规范

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

  1. FixedThreadPool 和 SingleThreadPool:
    允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM。
  2. CachedThreadPool:
    允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM。

线程池的几个重要参数介绍

  1. corePoolSize

线程池中的常驻核心线程数

在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。
当线程池中的线程数目到达 corePoolSize 后,就会把到达的任务放到缓存队列当中。

  1. maximumPoolSize

线程池能够容纳同时执行的最大线程数,此值必须大于等于1.

  1. keepAliveTime

多余的空闲线程的存活时间。当前线程池数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时,多余空闲线程会被销毁直到只剩下 corePoolSize 个线程为止。

默认情况下:
只有当线程池中的线程数大于 corePoolSize 时 keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize。

  1. unit

keepAliveTime 的单位

  1. workQueue

任务队列,被提交但尚未被执行的任务。

  1. threadFactory

表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。

  1. handler

拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数。

线程池的底层工作原理?

  1. 在创建了线程池后,等待提交过来的任务请求。

  2. 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断:

    1. 如果正在运行的线程数量 < corePoolSize,那么马上创建线程运行这个任务。
    2. 如果正在运行的线程数量 >= corePoolSize,那么将这个任务放入队列。
    3. 如果这个时候队列满了且正在运行的线程数量还 < maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务。
    4. 如果队列满了且正在运行的线程数量 >= maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

  4. 当一个线程无事可做超过一定的时间 (keepAliveTime) 时,线程池会判断:

    如果当前运行的线程数 > corePoolSize,那么这个线程就被停掉。
    所以线程池的所有任务完成后它最终会收缩到 corePoolSize 的大小。

线程池的拒绝策略

是什么

等待队列也满了,再也塞不下新任务了,同时线程池中的 max 线程也达到了,无法继续为新的任务服务。

这时候就需要拒绝策略机制合理的处理这个问题。

JDK 内置的拒绝策略

  1. AbortPolicy(默认):直接抛出 RejectedExecutionException 异常阻止系统正常运行。
  2. CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
  4. DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了 RejectedExecutionHandler 接口

合理配置线程池你是如何考虑的?

CPU密集型

CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。

CPU 密集型任务配置尽可能少的线程数量:

一般公式为:CPU 核数 + 1个线程的线程池。

IO密集型

  1. 由于 IO 密集型任务线程并不是一直执行任务,则应配置尽可能多的线程,如 CPU 核数 * 2
  2. IO 密集型,即该任务需要大量的 IO,即大量的阻塞。

在单线程上运行 IO 密集型的任务会导致浪费大量的 CPU 运算能力浪费在等待。

所以 IO 密集型任务中使用多线程可以大大的加速程序运行,即使在单核 CPU 上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO 密集型时,大部分线程都阻塞,故需要多配置线程数:

参考公式:CPU 核数 / (1 - 阻塞系数) 阻塞系数在 0.8-0.9 之间

比如 8 核 CPU:8 / (1 - 0.9) = 80 个线程数