线程池①——相关概念入门

132 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

1. 线程池的作用

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能线程池具备可拓展性 ,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行

2. 线程池的5种状态

RUNNING:线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。

SHUTDOWN不接收新任务,但能处理已排队的任务。 调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。

STOP不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。 调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。

TIDYING

  • SHUTDOWN 状态下,任务数为0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
  • 线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。
  • 线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。

TERMINATED线程池彻底终止。 线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。

image.png

3. Excutes

1️⃣ newFixedThreadPool

  • 创建一个固定的长度的线程池,每当提交一个任务就创建一个线程,知道达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会创建一个新的线程继续运行队列里的任务。

2️⃣ newSingleThreadExecutor

  • 这是一个单线程的 Executor ,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来代替它;它的特点是能确保依照任务在队列中的顺序来串行执行。

3️⃣ newCachedThreadPool

  • 创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。

4️⃣ newScheduledThreadPool

  • 创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于 Timer

5️⃣ newSingleThreadScheduledExecutor

  • 单线程可执行周期性任务的线程池

6️⃣ newWorkStealingPool

  • 任务窃取线程池,不保证执行顺序,适合任务耗时差异较大。线程池中有多个线程队列,有的线程队列中有大量的比较耗时的任务堆积,而有的线程队列却是空的,就存在有的线程处于饥饿状态,当一个线程处于饥饿状态时,它就会去其它的线程队列中窃取任务。解决饥饿导致的效率问题。默认创建的并行 level 是 CPU 的核数。主线程结束,即使线程池有任务也会立即停止。

4. 为什么不推荐Executors

Executors工具类创建的线程池队列或线程默认为Integer.MAX_VALUE,容易堆积请求

阿里巴巴Java开发手册:

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

推荐使用ThreadPoolExecutor类根据实际需要自定义创建

5. ThreadPoolExecutor

5.1 七大参数(⭐)

ThreadPoolExecutor类主要有以下七个参数:

  • int corePoolSize: 核心线程池大小,线程池中常驻线程的最大数量
  • int maximumPoolSize: 最大核心线程池大小(包括核心线程和非核心线程)
  • long keepAliveTime: 线程空闲后的存活时间(仅适用于非核心线程)
  • TimeUnit unit: 超时单位
  • BlockingQueue<Runnable> workQueue: 阻塞队列
  • ThreadFactory threadFactory: 线程工厂——创建线程的,一般默认
  • RejectedExecutionHandler handle: 拒绝策略

5.2 四大拒绝策略

拒绝策略就是当队列满时,线程如何去处理新来的任务。

1️⃣ AbortPolicy(中止策略)-默认

  • 功能:当触发拒绝策略时,直接抛出拒绝执行的异常
  • 使用场景:ThreadPoolExecutor中**默认的策略**就是AbortPolicy由于ExecutorService接口的系列ThreadPoolExecutor都没有显示的设置拒绝策略,所以默认的都是这个。

2️⃣ CallerRunsPolicy(调用者运行策略)

  • 功能:只要线程池没有关闭,就由提交任务的当前线程处理
  • 使用场景:一般在不允许失败、对性能要求不高、并发量较小的场景下使用。

3️⃣ DiscardPolicy(丢弃策略)

  • 功能:直接丢弃这个任务,不触发任何动作
  • 使用场景: 提交的任务无关紧要,一般用的少。

4️⃣ DiscardOldestPolicy(弃老策略)

  • 功能:抛弃下一个将要被执行的任务,相当于排队的时候把第一个人打死,然后自己代替
  • 使用场景:发布消息、修改消息类似场景。当老消息还未执行,此时新的消息又来了,这时未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。

5.3 工作队列

  • ArrayBlockingQueue: 使用数组实现的有界阻塞队列,特性先进先出
  • LinkedBlockingQueue: 使用链表实现的阻塞队列,特性先进先出,可以设置其容量,默认为Interger.MAX_VALUE
  • PriorityBlockingQueue: 使用平衡二叉树堆,实现的具有优先级的无界阻塞队列
  • DelayQueue: 无界阻塞延迟队列,队列中每个元素均有过期时间,当从队列获取元素时,只有过期元素才会出队列。队列头元素是最块要过期的元素。
  • SynchronousQueue: 一个不存储元素的阻塞队列,每个插入操作,必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态

6. 运行流程

具体的流程是这样的,首先会👇

判断线程池里的核心线程是否都在执行任务?

  • 否:调用/创建一个新的核心线程来执行任务

  • 是:如果在执行,就判断工作队列是否已满?

    • 否:将新提交的任务存储在工作队列里

    • 是:如果工作队列已满,就判断线程池里的线程数是否达到最大线程值?

      • 否:调用/创建一个新的非核心线程来执行任务
      • 是:执行线程池饱和策略

image.png

面试题

  1. 一个线程池 core 7; max 20 , queue 50, 问:100并发进来怎么分配的?

答:先有7个能直接得到执行, 接下来把50个进入队列排队等候, 在多开13个继续执行。 现在 70 个被安排上了。 剩下 30 个默认执行饱和策略。

  1. 线程池创建之后,会立即创建核心线程吗?

不会。在刚刚创建ThreadPoolExecutor 的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用了prestartCoreThread/prestartAllCoreThreads 事先启动核心线程。

7. 执行任务

  1. execute提交没有返回值,不能判断是否执行成功。只能提交一个Runnable的对象
  2. submit会返回一个Future对象,通过Future的get()方法来获取返回值,submit提交线程可以吃掉线程中产生的异常,达到线程复用。当get()执行结果时异常才会抛出。原因是通过submit提交的线程,当发生异常时,会将异常保存,待future.get()时才会抛出。

8. 关闭线程池

  • shutdown():不再继续接收新的任务,执行完成已有任务后关闭
  • shutdownNow():直接关闭,若果有任务尝试停止

9. 线程池出现异常会发生什么?

线程出现异常,线程会退出,并重新创建新的线程执行队列里任务,不能复用线程,当业务代码的异常捕获了,线程就可以复用

处理方法:

  • 使用ThreadFactory的UncaughtExceptionHandler保证线程的所有异常(即:全局异常)都能捕获(包括业务的异常),一般是兜底用的。如果提交方式用execute,不能复用线程
  • setUncaughtExceptionHandler + submit :可以吃掉异常并复用线程 (是吃掉,不报错)
  • setUncaughtExceptionHandler + submit + future.get() :可以获取到异常并复用线程

对于线程池异常的操作处理

  1. 提交线程的业务异常try catch处理,保证线程不会异常退出
  2. 业务之外的异常我们不可预见的,创建线程池设置ThreadFactory的UncaughtExceptionHandler可以对未捕获的异常做保底处理,通过submit提交任务,可以吃掉异常并复用线程;想要捕获异常这时用future.get()

\