Java基础–线程池

1. 为什么要使用线程池?

我们知道,操作系统创建线程、切换线程状态、终结线程都要进行CPU调度–这是一个耗费时间和系统资源的事情。服务端应用程序例如web应用中,比较常见的情况是:每当一个请求到达就创建一个新线程,然后在新线程中为请求服务。
每个请求对应一个线程(thread-per-request)方法的不足之一是:为每个请求创建一个新线程的开销很大;为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多。 除了创建和销毁线程的开销之外,活动的线程也消耗系统资源(线程的生命周期!)。在一个JVM里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或”切换过度”。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目。
线程池为线程生命周期开销问题和资源不足问题提供了解决方案。 通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且, 通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足

2. 线程池实现方式

一般一个简单线程池至少包含下列组成部分:

  1. 线程池管理器(ThreadPoolManager):用于创建并管理线程
  2. 工作线程(WorkThread):线程池中线程
  3. 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行
  4. 任务队列(WorkQueue):用于存放没有处理的任务,提供一种缓冲机制。

Doug Lea大师在 JDK1.5concurrent包中提供了 Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了 ExecutorService接口。但是阿里巴巴Java开发手册上有个建议: 【强制】线程池不允许使用 Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Java基础--线程池

ThreadPoolExecutor

阿里巴巴的JAVA开发手册推荐用ThreadPoolExecutor创建线程池。这里我们先来看看ThreadPoolExecutor创建线程池的api:

    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.

     *
     * @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 threadFactory the factory to use when the executor
     *        creates a new thread
     * @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:
     *         {@code corePoolSize < 0}
     *         {@code keepAliveTime < 0}
     *         {@code maximumPoolSize <= 0}
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    public ThreadPoolExecutor(int corePoolSize,  //&#x7EBF;&#x7A0B;&#x6C60;&#x6838;&#x5FC3;&#x6C60;&#x7684;&#x5927;&#x5C0F;
                              int maximumPoolSize, //&#x7EBF;&#x7A0B;&#x6C60;&#x7684;&#x6700;&#x5927;&#x7EBF;&#x7A0B;&#x6570;
                              long keepAliveTime, //&#x5F53;&#x7EBF;&#x7A0B;&#x6570;&#x5927;&#x4E8E;&#x6838;&#x5FC3;&#x65F6;&#xFF0C;&#x6B64;&#x4E3A;&#x7EC8;&#x6B62;&#x524D;&#x591A;&#x4F59;&#x7684;&#x7A7A;&#x95F2;&#x7EBF;&#x7A0B;&#x7B49;&#x5F85;&#x65B0;&#x4EFB;&#x52A1;&#x7684;&#x6700;&#x957F;&#x65F6;&#x95F4;&#x3002;
                              TimeUnit unit, //keepAliveTime &#x7684;&#x65F6;&#x95F4;&#x5355;&#x4F4D;
                              BlockingQueue<runnable> workQueue, //&#x7528;&#x6765;&#x50A8;&#x5B58;&#x7B49;&#x5F85;&#x6267;&#x884C;&#x4EFB;&#x52A1;&#x7684;&#x961F;&#x5217;&#x3002;
                              ThreadFactory threadFactory, //&#x7EBF;&#x7A0B;&#x5DE5;&#x5382;&#x3002;
                              RejectedExecutionHandler handler) //&#x62D2;&#x7EDD;&#x7B56;&#x7565;&#x3002;
</runnable></=>

其中阻塞队列:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
  • DelayQueue: 一个使用优先级队列实现的无界阻塞队列
  • SynchronousQueue:一个不存储元素的阻塞队列
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

拒绝策略:

  • ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务。(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

【补充说明】默认的拒绝策略,如果没有处理异常,系统会直接挂起,出现wait状态,如果处理了异常会直接丢掉这个任务接着往下执行,实际使用过程中需要注意。

在弄明白构造函数中的各个参数后,我们就可以灵活的设置一个线程池了,例如:

/**
* &#x83B7;&#x53D6;cpu&#x6838;&#x5FC3;&#x6570;
*/
 private static int corePoolSize = Runtime.getRuntime().availableProcessors();

    /**
     * corePoolSize&#x7528;&#x4E8E;&#x6307;&#x5B9A;&#x6838;&#x5FC3;&#x7EBF;&#x7A0B;&#x6570;&#x91CF;
     * maximumPoolSize&#x6307;&#x5B9A;&#x6700;&#x5927;&#x7EBF;&#x7A0B;&#x6570;
     * keepAliveTime&#x548C;TimeUnit&#x6307;&#x5B9A;&#x7EBF;&#x7A0B;&#x7A7A;&#x95F2;&#x540E;&#x7684;&#x6700;&#x5927;&#x5B58;&#x6D3B;&#x65F6;&#x95F4;
     */
    public static ThreadPoolExecutor executor  = new ThreadPoolExecutor(corePoolSize, corePoolSize+1, 10l, TimeUnit.SECONDS,
            new LinkedBlockingQueue<runnable>(1000));
</runnable>

Executors

我们再补充看一下 Executors提供的工厂方法,看过源码的都知道其实这些这些工厂方法里面调用的还是ThreadPoolExecutor的构造函数:

  • newSingleThreadExecutor
    创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
    此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())
  • newFixedThreadPool
    创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
  • newCachedThreadPool
    创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。 new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue());
  • newScheduledThreadPool
    创建一个定长线程池,支持定时及周期性任务执行 new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
    new DelayedWorkQueue());

【比较说明】Executors 各个方法的弊端:

  1. newFixedThreadPool和newSingleThreadExecutor:
    主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
  2. newCachedThreadPool 和 newScheduledThreadPool:
    主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

3.线程池工作原理(执行流程)

线程池的工作原理和执行流程,可以通过以下两张图来进行展示

Java基础--线程池
Java基础--线程池
  1. 在创建了线程池后,等待提交过来的任务请求
  2. 当调用execute()方法添加一个请求任务时,线程池会做如下判断:
  3. 如果正在运行的线程数量小于corePoolSize,那么马上创建马上创建线程运行这个任务。
  4. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个 任务放入队列
  5. 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务。
  6. 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会 启动饱和拒绝策略来执行
  7. 当一个线程完成任务时,它会从队列中取下一个任务来执行
  8. 当一个线程无事可做超过一定的时间(keepAlilveTime)时,线程池会判断:
  9. 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
  10. 所以线程池的所有任务完成后它 最终会收缩到corePoolSize的大小

总结成白话就是:

  1. 创建线程池之后,有任务提交给线程池,会先由 核心线程执行
  2. 如果任务持续增加,corePoolSize用完并且任务队列满了,这个时候线程池会增加线程的数量,增大到最大线程数
  3. 这个时候如果任务继续增加,那么由于线程数量已经达到最大线程数,等待队列也已经满了,这个时候线程池实际上是没有能力执行新的任务的,就会采用拒绝策略
  4. 如果任务量下降,就会有很多线程是不需要的,无所事事,而只要这些线程空闲的时间超过空闲线程时间,就会被销毁,直到剩余线程数为corePoolSize。

4.线程池使用注意事项

最后想谈谈使用注意事项,其实主要是合理配置线程池大小,这个要分析任务特性,主要从以下几个方面进行分析:

  • 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  • 任务的优先级:高,中和低。
  • 任务的执行时间:长,中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

根据任务所需要的cpu和io资源的量可以分为:

  • CPU密集型任务: 主要是执行计算任务,响应时间很快,cpu一直在运行,这种任务cpu的利用率很高。
  • IO密集型任务:主要是进行IO操作,执行IO操作的时间较长,这是cpu出于空闲状态,导致cpu的利用率不高。

为了合理最大限度的使用系统资源同时也要保证的程序的高性能,可以给CPU密集型任务和IO密集型任务配置一些线程数。

  • CPU&#x5BC6;&#x96C6;&#x578B;:线程个数为CPU核数。这几个线程可以并行执行,不存在线程切换到开销,提高了cpu的利用率的同时也减少了切换线程导致的性能损耗
  • IO&#x5BC6;&#x96C6;&#x578B;:线程个数为CPU核数的两倍。到其中的线程在IO操作的时候,其他线程可以继续用cpu,提高了cpu的利用率。

如果你读完了,希望能有所帮助!

参考文章
要就来15道多线程面试题一次爽到底(1.1w字用心整理)

Original: https://www.cnblogs.com/xieshuang/p/12674596.html
Author: 河岸飞流
Title: Java基础–线程池

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/571644/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • SpringBoot 属性配置文件数据注入配置和yml与properties区别

    前言 我们知道 SpringBoot 通过配置类来解放一堆的xml文件配置,通属性配置文件,来进行,系统全局属性配置,这样极大的简化了我们开发过程,java web 也可以甜甜的从…

    Java 2023年6月13日
    080
  • 12、SpringBoot 启动 刷新应用上下文 启动WEB容器

    目录:Springboot源码学习目录上文:11、SpringBoot 启动 刷新应用上下文 自动装配解析(三)前言:好了,终于将刷新应用上下文的自动装配流程的源码读完了,顺便也将…

    Java 2023年6月13日
    076
  • java 启动脚本

    #!bin/bash source /etc/profile MEM_TOTAL=cat /proc/meminfo | grep 'MemTotal' | a…

    Java 2023年5月29日
    066
  • redis 入门安装流程

    redis安装流程 安装linux的Redis [官网下载即可][ https://redis.io/download/ ] 一般会移动到opt目录下 mv redis-7.0.4…

    Java 2023年6月16日
    066
  • 使用系统参数表,提升系统的灵活性

    1、使用系统参数表的好处 ​ Spring Boot项目中常有一些相对稳定的参数设置项,其作用范围是系统级的或模块级的,这些参数称为系统参数。这些变量以参数形式进行配置,从而提高变…

    Java 2023年6月14日
    094
  • 拜托,面试官别问我「位图」了

    这是之前面试的时候面试官问到过的一个问题,今天正好看到 布隆过滤器,写篇文章总结一下 我们先看一下流程,流程懂了,问题就解决 90%了 我们都知道一个 int 占 4字节,一个字节…

    Java 2023年6月9日
    090
  • 线程同步的情景之二

    情景二:数量有限,先到先得 情景简介:与情景一类似,但是这次茅坑的数量不只一个。如果有需求的人数少于茅坑数量,那一切都很和谐。但是人数超过茅坑数量的时候该怎么办?多个人占用一个坑?…

    Java 2023年5月30日
    094
  • Security框架中使用FastJson反序列化SimpleGrantedAuthority

    让FastJson支持SimpleGrantedAuthority等没有无参构造函数类的反序列化 &#x5728;Spring Security&#x6846;&a…

    Java 2023年6月8日
    059
  • 最短路

    最短路 文章目录 最短路 一、最短路相关概念 二、Floyd 三、Dijkstra 四、Bellman-Ford 一、最短路相关概念 ; 二、Floyd 我们定义一个数组 dis[…

    Java 2023年6月5日
    0107
  • ZendFramework学习第二章(Json)

    使用Zendframework中的Zend_Json组件可以实现PHP对象与JSON对象之间的转换。 json是一种轻量级的数据交换格式。 轻量级: 1.这种格式不需要特定的容器。…

    Java 2023年5月29日
    097
  • JAVA Builder模式构建MAP/LIST的示例

    我们在构建一个MAP时,要不停的调用put,有时候看着觉得很麻烦,刚好,看了下builder模式,觉得这思路不错,于是乎,照着用builder模式写了一个构建MAP的示例,代码如下…

    Java 2023年5月29日
    0101
  • JavaWeb过滤器Filter(附tomcat部分源码分析)

    过滤器Filter 过滤器通常对一些web资源进行拦截,做完一些处理器再交给下一个过滤器处理,直到所有的过滤器处理器,再调用servlet实例的service方法进行处理。过滤器可…

    Java 2023年6月9日
    074
  • 5 信息的表示和处理_整数表示

    1 无符号数编码 2 补码编码 3 有符号数和无符号数之间的转换 3.1 补码转为无符号数 3.2 无符号数转为补码 4 数字的位扩展 5 数字的位截断 6 C跟Java对无符号数…

    Java 2023年6月7日
    088
  • 解决Jenkins构建完成之后服务自动关闭的问题

    jenkins默认会在构建完成后杀掉构建过程中又jenkins中shell命令触发的衍生进程。 在shell命令中加上BUILD_ID=dontKillMe可以阻止进程被杀掉。 B…

    Java 2023年6月7日
    088
  • 第一个微信小项目

    第一个好友分析: 我们需要用到wxpy这个库,这个库用到时会弹出一个二维码,这个二维码是通过扫码的方式登录微信,以获取信息 1 #导入模块 2 from wxpy import *…

    Java 2023年6月6日
    074
  • Java 并发编程小册整理好了

    Java 有并发,并发知识之大,一口吃不下这曾是我不愿意触碰的知识角多次一头扎进并发,无功而返为应对面试,临时苦苦记忆,不成体系这一次我决定从基础开始,攻克它 12,0000 字6…

    Java 2023年6月5日
    083
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球