Java基础--线程池

1. 为什么要使用线程池? 我们知道,操作系统创建线程、切换线程状态、终结线程都要进行CPU调度--这是一个耗费时间和系统资源的事情。服务端应用程序例如web应用中,比较常见的情况是:...



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的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
在这里插入图片描述

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:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    public ThreadPoolExecutor(int corePoolSize,  //线程池核心池的大小
                              int maximumPoolSize, //线程池的最大线程数
                              long keepAliveTime, //当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
                              TimeUnit unit, //keepAliveTime 的时间单位
                              BlockingQueue<Runnable> workQueue, //用来储存等待执行任务的队列。
                              ThreadFactory threadFactory, //线程工厂。
                              RejectedExecutionHandler handler) //拒绝策略。

其中阻塞队列:

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

拒绝策略:

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

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

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

/**
* 获取cpu核心数
*/
 private static int corePoolSize = Runtime.getRuntime().availableProcessors();

    /**
     * corePoolSize用于指定核心线程数量
     * maximumPoolSize指定最大线程数
     * keepAliveTime和TimeUnit指定线程空闲后的最大存活时间
     */
    public static ThreadPoolExecutor executor  = new ThreadPoolExecutor(corePoolSize, corePoolSize+1, 10l, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(1000));

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.线程池工作原理(执行流程)

线程池的工作原理和执行流程,可以通过以下两张图来进行展示
在这里插入图片描述
在这里插入图片描述

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

总结成白话就是:

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

4.线程池使用注意事项

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

  • 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。

  • 任务的优先级:高,中和低。

  • 任务的执行时间:长,中和短。

  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

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

  • CPU密集型任务: 主要是执行计算任务,响应时间很快,cpu一直在运行,这种任务cpu的利用率很高。

  • IO密集型任务:主要是进行IO操作,执行IO操作的时间较长,这是cpu出于空闲状态,导致cpu的利用率不高。

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

  • CPU密集型:线程个数为CPU核数。这几个线程可以并行执行,不存在线程切换到开销,提高了cpu的利用率的同时也减少了切换线程导致的性能损耗

  • IO密集型:线程个数为CPU核数的两倍。到其中的线程在IO操作的时候,其他线程可以继续用cpu,提高了cpu的利用率。


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

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

  • 发表于 2020-04-10 19:33
  • 阅读 ( 168 )
  • 分类:网络文章

条评论

请先 登录 后评论
不写代码的码农
小编

篇文章

作家榜 »

  1. 小编 文章
返回顶部
部分文章转自于网络,若有侵权请联系我们删除