Java 并发之线程池

开发 后端
JDK 在并发包中为我们定义了一套 Executor 框架,帮助开发人员有效地进行线程控制,有基础的线程池类、有线程池工厂,但是最最重要还是 ThreaPoolExecutor,也是面试中最常问的知识点。本文重点介绍 ThreaPoolExecutor 的原理。

[[340705]]

 本文转载自微信公众号「怀梦追码」,作者水目沾 。转载本文请联系怀梦追码公众号。 

线程池的作用

池化技术是一种很常见的计算机技术,主要是为了复用和提高性能,如内存池、连接池、对象池等。线程池也不例外,他的主要作用如下:

  • 提高性能:线程的频繁创建和销毁会产生的很大的系统开销,线程池中的线程复用可以大幅度的减少这种不必要的开销。
  • 复用和管理:方便对池子中的线程进行管理和复用,避免在生产环境中大量的创建线程。
  • 解耦:只暴露提交任务的接口,将线程池的创建、销毁等工作与业务解耦。

JDK 在并发包中为我们定义了一套 Executor 框架,帮助开发人员有效地进行线程控制,有基础的线程池类、有线程池工厂,但是最最重要还是 ThreaPoolExecutor,也是面试中最常问的知识点。本文重点介绍 ThreaPoolExecutor 的原理。

线程池的参数说明

  1. ThreaPoolExecutor( 
  2.     int corePoolSize, 
  3.     long keepAliveTime, 
  4.     TimeUnit unit, 
  5.     BlockingQueue<Runnable> workQueue, 
  6.     ThreadFactory threadFactory, 
  7.     RejectedExecutionHandler handler) 

ThreaPoolExecutor 参数的含义如下

  • corePoolSize: 线程池中核心线程的数量。
  • maximumPoolSize: 线程池中的最大线程数量。
  • keepAliveTime: 当线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间。即超过 coolPoolSize 的空闲线程在 多长时间内,会被销毁。
  • unit: keepAliveTime 的单位,可以为 时、分、秒等多种值。
  • workQueue: 任务队列,存放被提交但尚未被执行的任务。
  • threadFactory: 线程工厂,用于创建线程,一般用默认即可。
  • handler: 拒绝策略,当线程池处理不过来任务时,如何拒绝任务。

以上参数中 workQueue、threadFactory、handler 相对复杂,需要单独介绍,下面主要介绍下 ThreadFactory 和 RejectedExecutionHandler

1. 线程工厂:ThreadFactory

线程池中的线程都由 TrheadFactory 定义的线程工厂来创建,它是一个接口只有 Thread newThread(Runnable r) 方法,用来创建线程。虽然创建 ThreadPoolExecutor 的时候可以不指定该参数,但是阿里巴巴编码规约建议最好指定该参数,有以下几个好处:

  • 跟踪线程池在何时、创建了多少线程。
  • 可以自定义线程池的名称、组以及优先级等信息。
  • 设置线程的其他状态等,如守护进程。

2. 拒绝策略:RejectedExecutionHandler

当线程池线程数量达到 maxPoolSize 大小时,再提交新的任务会执行拒绝策略,JDK 定义了四种拒绝策略:

  • AbortPolicy 该策略直接抛出异常
  • CallerRunsPolicy 调用者线程处理任务,该策略并不是真正的丢弃任务,会让当前线程来执行被抛弃的任务,由于只有一个线程,所有的任务会被串行执行。
  • DiscardOldestPolicy 丢弃最老的一个请求,即队列头部的即将被执行的任务,并尝试再次提交当前任务。
  • DiscardPolicy 该策略默默丢弃无法处理的任务。

以上四种拒绝策略都继承了接口 RejectedExecutionHandler 并实现该接口的 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。如果以上四种拒绝策略都满足不了你的需求,可以自定义拒绝策略,继承接口 RejectedExecutionHandler 并实现方法即可。

线程池的调度逻辑

ThreaPoolExecutor 对提交的任务处理逻辑如下图,

 

1. 提交任务时:

  • 如果线程池中的线程数小于 corePoolSize (无论是否有空闲线程),创建新的线程(谓之核心线程)来处理。
  • 如果线程池中的线程数已经大于或者 corePoolSize ,新提交的任务将被放置到等候队列中,等待调度。
  • 如果等待队列已满,并且线程池中的线程数量小于 maxPoolSize,将继续创建新线程处理任务。
  • 如果队列已满且线程数量也达到了上限,将使用拒绝策略来处理。

2. 任务进行中时:

当队列中的任务已经执行完,部分线程开始空闲,非核心线程会在空闲后的 keepAliveTime 的时间内自行销毁。

而空闲核心线程是否退出取决于线程池的另一个参数 allowCoreThreadTimeOut 。当配置为 true 的时候,即使是核心线程,超时也会退出。

线程池的生命周期

线程池同线程一样也有自己的生命周期,包括 RUNNING、SHUTDOWN、STOP、TIDYING 和 TERMINATED 五种状态,他们的转换关系如下图,并且这些转换时不可逆的。

 

1. RUNNING

该状态是线程池的工作状态,能够接受新任务以及对接受的任务进行处理。线程池的初始状态,即线程创建成功后就处理此状态。

2. SHUTDOWN

关闭状态,线程池不再接受新的任务,但是能继续处理提交到线程池中的任务。线程状态 RUNNING 的情况下调用 shutdown() 方法进入该状态。

3. STOP

停止状态,线程池不接受新的任务,也不处理阻塞队列中的任务,同时会中断正在执行任务的线程。在线程处于 RUNNING 或者 SHUTDOWN 状态下调用 shutdownNow() 方法进入该状态。

4. TIDYING

所有任务都销毁了,workCount 为 0,会自动从 RUNNING 或者 STOP 状态转化为 TIDYING 状态。在转换过程中会调用 terminated() 方法,ThreadPoolExecutor 类的 ternimated() 方法为空,如果想在线程池变成 TIDYING 的时候有所处理,可以重载该方法。

线程池在 SHUTDOWN 状态下,阻塞队列为空并且执行任务为空时转换为 TIDYING 状态;线程池在 STOP 状态下,执行的任务为空时转换为 TIDYING 状态。

5. TERMINATED

结束状态,线程池的最终状态,该状态的线程池不会再有任何操作。线程池执行 terminated() 方法后处于该状态。

JDK 四种线程池

了解 ThreaPoolExecutor 的基本原理后再来看看 JDK 在 Executors 中为开发人员定义的四个线程池工厂方法,其实它们内部调用的是 ThreaPoolExecutor,只是使用了不同的参数,下面来了解下它们的特性。

newFixedThreadPool() 方法:该方法返回一个固定线程池数量的线程,提交任务时如果线程池中有空闲线程,则立即执行,没有则新的任务会被缓存在一个任务队列中,它创建线程池的代码如下:

  1. public static ExecutorService newFixedThreadPool(int nthread) { 
  2.   return new ThreadPoolExecutor(nthread,  
  3.                                 nthread,  
  4.                                 0L,  
  5.                                 TimeUnit.MILLSECCONDS,  
  6.                                 new LikedBlockingQueue<Runnable>()) 

newSingleThreadExecutor() 方法:该方法返回只有一个线程的线程池,如果多余的任务提交到线程池,则被提交到任务队列中。它创建线程池代码如下:

  1. public static ExecutorService newSingleThreadExecutor() { 
  2.   return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,  
  3.                                                                         1, 
  4.                                                                         0L, TimeUnit.MILLISECONDS, 
  5.                                                                         new LinkedBlockingQueue<Runnable>())); 

newCachedThreadPool() 方法:该方法返回一个可根据实际情况调整线程数的线程池,它的核心线程数为 0 ,线程总数为 Integer.MAX_VALUE ,队列采用的是 SynchronousQueue,这样即使线程满,任务也不能提交到队列中。

  1. public static ExecutorService newCachedThreadPool() { 
  2.         return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 
  3.                                       60L, TimeUnit.SECONDS, 
  4.                                       new SynchronousQueue<Runnable>()); 
  5.     } 

newScheduledThreadPool():该方法一个固定长度的线程池,并且以延迟或者定时的方式去执行任务。它的队列使用 DelayedWorkQueue,所以任务必须继承 Delay 接口。

  1. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { 
  2.     return new ScheduledThreadPoolExecutor(corePoolSize); 
  3.  
  4. public ScheduledThreadPoolExecutor(int corePoolSize) { 
  5.     super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, 
  6.           new DelayedWorkQueue()); 

 

责任编辑:武晓燕 来源: 怀梦追码
相关推荐

2017-01-10 13:39:57

Python线程池进程池

2012-02-01 11:20:23

Java线程

2013-05-23 15:59:00

线程池

2022-11-09 09:01:08

并发编程线程池

2023-06-07 13:49:00

多线程编程C#

2021-09-11 07:32:15

Java线程线程池

2020-12-08 08:53:53

编程ThreadPoolE线程池

2023-07-11 08:34:25

参数流程类型

2015-03-25 17:57:50

JavaJava糟糕

2021-07-03 17:44:34

并发高并发原子性

2012-05-15 02:18:31

Java线程池

2012-02-29 13:26:20

Java

2011-12-29 13:31:15

Java

2021-09-18 06:56:01

JavaCAS机制

2020-11-25 11:33:47

Java线程技术

2015-08-20 09:17:36

Java线程池

2023-08-09 09:03:49

CPU密集型运算

2024-11-06 09:39:52

2023-11-22 08:37:40

Java线程池

2021-09-11 15:26:23

Java多线程线程池
点赞
收藏

51CTO技术栈公众号