快捷搜索:  汽车  科技

线程池的核心参数(一文搞懂线程池中的几个常用参数)

线程池的核心参数(一文搞懂线程池中的几个常用参数)2)newCachedThreadPool和newScheduledThreadPool:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。1.在创建线程池的时候,大部分人还是会选择使用Executors去创建。不推荐使用,阿里规范里对此有说法:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors各个方法的弊端:1)newFixedThreadPool和newSingleThreadExecutor:

这两天在准备面试,线程是绕不开的话题,在讨论线程的时候往往主要是说线程池的理解。于是乎翻遍各个博客,终于找到了一篇写得比较好的博客。在此基础上把看到的内容摘出来。

https://cnblogs.com/zjfjava/p/11227456.html(原文地址) 作者:雪山上的蒲公英

我们都知道 Executors的四种创建线程的方法,但是阿里却不建议使用,这是为什么呢》下面就一探究竟吧。

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。

1.在创建线程池的时候,大部分人还是会选择使用Executors去创建。不推荐使用,阿里规范里对此有说法:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors各个方法的弊端:

1)newFixedThreadPool和newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。

2)newCachedThreadPool和newScheduledThreadPool:

主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

2.看看ThreadPoolExecutor是如何处理的,我们先看一下如下的源码:

/** * Creates a new {@code ThreadPoolExecutor} with the given initial * parameters and default thread factory. * * @param corePoolSize the number of threads to keep in the pool even * if they are idle unless {@code allowCoreThreadTimeOut} is set * @param maximumPoolSize the maximum number of threads to allow in the * pool * @param keepAliveTime when the number of threads is greater than * the core this is the maximum time that excess idle threads * will wait for new tasks before terminating. * @param unit the time unit for the {@code keepAliveTime} argument * @param workQueue the queue to use for holding tasks before they are * executed. This queue will hold only the {@code Runnable} * tasks submitted by the {@code execute} method. * @param handler the handler to use when execution is blocked * because the thread bounds and queue capacities are reached * @throws IllegalArgumentException if one of the following holds:<br> * {@code corePoolSize < 0}<br> * {@code keepAliveTime < 0}<br> * {@code maximumPoolSize <= 0}<br> * {@code maximumPoolSize < corePoolSize} * @throws NullPointerException if {@code workQueue} * or {@code handler} is null */ public ThreadPoolExecutor(int corePoolSize int maximumPoolSize long keepAliveTime TimeUnit unit BlockingQueue<Runnable> workQueue RejectedExecutionHandler handler) { this(corePoolSize maximumPoolSize keepAliveTime unit workQueue Executors.defaultThreadFactory() handler); }

ThreadPoolExecutor 是线程池的核心实现。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。

下面是对其参数的解释,在创建线程池时需根据自己的情况来合理设置线程池。

corePoolSize & maximumPoolSize

核心线程数(corePoolSize)和最大线程数(maximumPoolSize)是线程池中非常重要的两个概念,希望同学们能够掌握。当一个新任务被提交到池中,如果当前运行线程小于核心线程数(corePoolSize),即使当前有空闲线程,也会新建一个线程来处理新提交的任务;如果当前运行线程数大于核心线程数(corePoolSize)并小于最大线程数(maximumPoolSize),只有当等待队列已满的情况下才会新建线程。

keepAliveTime & unit

keepAliveTime 为超过 corePoolSize 线程数量的线程最大空闲时间,unit 为时间单位。

BlockingQueue

任何阻塞队列(BlockingQueue)都可以用来转移或保存提交的任务,线程池大小和阻塞队列相互约束线程池:

(1).如果运行线程数小于corePoolSize,提交新任务时就会新建一个线程来运行;

(2).如果运行线程数大于或等于corePoolSize,新提交的任务就会入列等待;如果队列已满,并且运行线程数小于maximumPoolSize,也将会新建一个线程来运行;

(3).如果线程数大于maximumPoolSize,新提交的任务将会根据拒绝策略来处理。

下面来看一下三种通用的入队策略:

直接传递:通过 SynchronousQueue 直接把任务传递给线程。如果当前没可用线程,尝试入队操作会失败,然后再创建一个新的线程。

无界队列:使用无界队列(如 LinkedBlockingQueue)作为等待队列,当所有的核心线程都在处理任务时, 新提交的任务都会进入队列等待。

有界队列:当使用有限的最大线程数时,有界队列(如 ArrayBlockingQueue)可以防止资源耗尽,但是难以调整和控制。

handler

拒绝策略 当线程池已经关闭或达到饱和(最大线程和队列都已满)状态时,新提交的任务将会被拒绝。ThreadPoolExecutor 定义了四种拒绝策略:

AbortPolicy:默认策略,在需要拒绝任务时抛出RejectedExecutionException;

CallerRunsPolicy:直接在 execute 方法的调用线程中运行被拒绝的任务,如果线程池已经关闭,任务将被丢弃;

DiscardPolicy:直接丢弃任务;

DiscardOldestPolicy:丢弃队列中等待时间最长的任务,并执行当前提交的任务,如果线程池已经关闭,任务将被丢弃。

我们也可以自定义拒绝策略,只需要实现 RejectedExecutionHandler;需要注意的是,拒绝策略的运行需要指定线程池和队列的容量。

3.通过下面的demo来了解ThreadPoolExecutor创建线程的过程。

package com.bw.auto.aspect.test; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolSerialTest { public static void main(String[] args) { //核心线程数 int corePoolSize = 3; //最大线程数 int maximumPoolSize = 6; //超过 corePoolSize 线程数量的线程最大空闲时间 long keepAliveTime = 2; //以秒为时间单位 TimeUnit unit = TimeUnit.SECONDS; //创建工作队列,用于存放提交的等待执行任务 BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2); ThreadPoolExecutor threadPoolExecutor = null; try { //创建线程池 threadPoolExecutor = new ThreadPoolExecutor(corePoolSize maximumPoolSize keepAliveTime unit workQueue new ThreadPoolExecutor.AbortPolicy()); //循环提交任务 for (int i = 0; i < 9; i ) { //提交任务的索引 final int index = (i 1); threadPoolExecutor.submit(() -> { //线程打印输出 System.out.println("大家好,我是线程:" Thread.currentThread().getName() "======" index); try { //模拟线程执行时间,10s Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } }); //每个任务提交后休眠500ms再提交下一个任务,用于保证提交顺序 Thread.sleep(500); } } catch (InterruptedException e) { e.printStackTrace(); } finally { threadPoolExecutor.shutdown(); } } }

执行结果:

线程池的核心参数(一文搞懂线程池中的几个常用参数)(1)

执行效果图

这里描述一下执行的流程:

  • (1).首先通过 ThreadPoolExecutor 构造函数创建线程池;
  • (2).执行 for 循环,提交 8 个任务(恰好等于maximumPoolSize[最大线程数] capacity[队列大小]);
  • (3).通过 threadPoolExecutor.submit 提交 Runnable 接口实现的执行任务;
  • (4).提交第1个任务时,由于当前线程池中正在执行的任务为 0 ,小于 3(corePoolSize 指定),所以会创建一个线程用来执行提交的任务1;
  • (5).提交第 2, 3 个任务的时候,由于当前线程池中正在执行的任务数量小于等于 3 (corePoolSize 指定),所以会为每一个提交的任务创建一个线程来执行任务;
  • (6).当提交第4个任务的时候,由于当前正在执行的任务数量为 3 (因为每个线程任务执行时间为10s,所以提交第4个任务的时候,前面3个线程都还在执行中),此时会将第4个任务存放到 workQueue 队列中等待执行;
  • (7).由于 workQueue 队列的大小为 2 ,所以该队列中也就只能保存 2 个等待执行的任务,所以第5个任务也会保存到任务队列中;
  • (8).当提交第6个任务的时候,因为当前线程池正在执行的任务数量为3,workQueue 队列中存储的任务数量也满了,这时会判断当前线程池中正在执行的任务的数量是否小于6(maximumPoolSize指定);
  • (9).如果小于 6 ,那么就会新创建一个线程来执行提交的任务 6;
  • (10).执行第7,8个任务的时候,也要判断当前线程池中正在执行的任务数是否小于6(maximumPoolSize指定),如果小于6,那么也会立即新建线程来执行这些提交的任务;

  • 此时,6个任务都已经提交完毕,那 workQueue 队列中的等待 任务4 和 任务5 什么时候执行呢?
  • (11).当任务1执行完毕后(10s后),执行任务1的线程并没有被销毁掉,而是获取 workQueue 中的任务4来执行;
  • (12).当任务2执行完毕后,执行任务2的线程也没有被销毁,而是获取 workQueue 中的任务5来执行;

通过上面流程的分析,也就知道了之前案例的输出结果的原因。其实,线程池中会线程执行完毕后,并不会被立刻销毁,线程池中会保留 corePoolSize 数量的线程,当 workQueue 队列中存在任务或者有新提交任务时,那么会通过线程池中已有的线程来执行任务,避免了频繁的线程创建与销毁,而大于 corePoolSize 小于等于 maximumPoolSize 创建的线程,则会在空闲指定时间(keepAliveTime)后进行回收。

最后再看一下拒绝策略,在上面的测试中,我设置的执行线程总数恰好等于maximumPoolSize[最大线程数] capacity[队列大小],因此没有出现需要执行拒绝策略的情况,因此在这里,我再增加一个线程,提交9个任务,来演示不同的拒绝策略。

线程池的核心参数(一文搞懂线程池中的几个常用参数)(2)

AbortPolicy拒绝策略

线程池的核心参数(一文搞懂线程池中的几个常用参数)(3)

CallerRunsPolicy

线程池的核心参数(一文搞懂线程池中的几个常用参数)(4)

DiscardPolicy

线程池的核心参数(一文搞懂线程池中的几个常用参数)(5)

DiscardOldestPolicy

至此整个线程池就搞清楚了,各个重要的参数意义也很明确了,对拒绝策略有了更深的理解。

https://cnblogs.com/zjfjava/p/11227456.html(原文地址) 作者:雪山上的蒲公英

猜您喜欢: