19

Java 并发之线程池

 3 years ago
source link: http://developer.51cto.com/art/202009/625392.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

eAFZJb7.jpg!mobile

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

线程池的作用

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

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

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

线程池的参数说明

ThreaPoolExecutor( 
    int corePoolSize, 
    long keepAliveTime, 
    TimeUnit unit, 
    BlockingQueue<Runnable> workQueue, 
    ThreadFactory threadFactory, 
    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 对提交的任务处理逻辑如下图,

beIVr2a.png!mobile

1. 提交任务时:

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

2. 任务进行中时:

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

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

线程池的生命周期

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

VVbqYjA.png!mobile

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

public static ExecutorService newFixedThreadPool(int nthread) { 
  return new ThreadPoolExecutor(nthread,  
                                nthread,  
                                0L,  
                                TimeUnit.MILLSECCONDS,  
                                new LikedBlockingQueue<Runnable>()) 
} 

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

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

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

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

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

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { 
    return new ScheduledThreadPoolExecutor(corePoolSize); 
} 
 
public ScheduledThreadPoolExecutor(int corePoolSize) { 
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, 
          new DelayedWorkQueue()); 
} 

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK