线程池的使用

1,103 阅读7分钟

线程池的正确使用时java高并发的基础,你曾经因为不了解线程池导致OOM吗?曾经因为不了解队列缓存导致任务丢失吗?曾经因为错误填写核心线程数导致逾期的执行效果不能达到吗?让我们来一起来快速的了解线程池的使用吧!

预定义线程池的种类

java.util.concurrent包下的Executors类中包含6种已经实现好的static线程池工具

  • newFixedThreadPool
    • init方法可传入固定线程数
    • 创建可重用的且固定数量的线程的线程池,操作共享的无边界队列
    • 在任何时间点,固定数量的线程都是活跃的。如果所有线程都处在执行任务状态,又提交了其他任务,那么这些任务会在队列中等待可用的线程。
    • 一个线程在执行完成之前由于失败被终止的话,如果需要可以更换一个新的线程执行后续任务。
    • 线程池中的线程会一直的持续下去,直到有程序明确执行了ExecutorService#shutdown方法之后。
  • newWorkStealingPool
    • init方法可传入并行执行级别
    • 线程池中存在一定数量的线程,并且足够支持给定并行级别,并且可以使用多个队列来减少任务间互相争用线程的情况。
    • 并行级别对应于主动参与活可用于执行任务处理的最大线程池数,实际线程数可能动态的增加或减少。
    • 这种线程池不能保证提交任务的执行顺序。
    • 另外需要注意,如果初始化是不指定并行级别的话,会默认取Runtime.getRuntime().availableProcessors(),在不同主机配置不同插槽不同的情况下,这个参数返回的数据也是不准确的,甚至有返回null的情况下。
  • newSingleThreadExecutor
    • 创建一个执行器,该执行器使用单个工作线程在无界队列中操作,注意队列的最大值是Intenger最大值。
    • 如果单个由于在关闭之前的执行过程中由于失败而中止,则在需要执行后续任务是,将替换一个新的线程继续执行。
    • 保证任务按顺序执行并完成,在任何给定的时间内不会有多个任务处于活跃状态。
    • 与其他的执行器不同,他会保证返回的执行器对象不可重新配置以使用额外配置的线程。
  • newCachedThreadPool
    • 创建一个线程池,线程池中的线程书数量根据当前需要进行创建,并且会重用之前的构造线程。
    • 这样的线程池可以提高短时间内较多的异步执行程序的性能。
    • 对于没有可用线程的情况,则会创建线程并将其添加到线程池中,并且具有60s呗未使用的将被终止并移除缓存。因此,一个足够长时间的闲置线程池将不会消耗过多的资源。
  • newScheduledThreadPool
    • 创建一个线程池,该线程池可以安排命令在给定的延迟后运行或定期执行。
  • newSingleThreadScheduledExecutor
    • 同上,但是线程池内线程数量唯一,唯一性与newSingleThreadExecutor相同。

推荐线程池的使用方式

阿里为大家提供的java开发规约上明确表明了

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1. FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2. CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

既然大佬们都这样说了,那我们就只能自己来通过ThreadPoolExecutor来实现线程池来了,那么接下来看看使用ThreadPoolExecutor要怎么实现线程池工具了!!!

   /**
     * 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,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

先来看一下源码大佬为我们的描述吧

corePoolSize(核心线程数):池中要保留的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut。

maximumPoolSize(最大线程池大小):池中允许的最大并行执行的线程数。

keepAliveTime(保持活跃时间):当线程池中的活跃线程数大于核心线上数时,这个时间是多余的空闲线程在终止前等待新任务的最长时间(看大佬们描述的多么简洁)。

unit(时间单位):_keepAliveTime的时间单位。使用的是TimeUnit类。

workQueue(工作队列):小伙伴们也发现了,之前Executors类为我们提供好的线程池工具类中,其实主要就是对这个工作队列的疯狂操作吧,我们再看看大佬是怎么说的吧——‘在任务执行前用于保留任务的队列。此队列将只保存{@code execute}方法提交的{@code Runnable}任务’。

ThreadFactory(线程工厂):执行器创建新线程时要使用的工厂,工厂的意义大家可能比我更懂,这里的好处应该就是规范了线程对象输出,添加一个线程名称啊之类的。

handler(处理器):其实就是处理线程任务的一种策略,当执行因达到线程界限和队列容量而被阻止时要使用的处理程序

真正值得注意的其实是各个关键字之间的关系corePoolSize、maximumPoolSize、workQueue这个之前我一直理解的都不正确,看下面这部分代码

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
         /*
         * 下面拢共分了三部分,简单来说一下:
         * 1.当 工作线程数 < corePoolSize(核心线程数)时,
         * 执行addWorker方法,成功即返回,失败即继续。
         * 
         * 2.当 任务添加队列成功,会两次检验线程是否被停止,有必要的话就停止排队,拒绝执行;
         * 或者启动一个新线程去执行。
         *
         * 3.核心线程满了,队列满了,就会创建新的线程执行,知道达到最大线程数,否则的话
         * 会执行拒绝策略。
         */
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

上面的内容可以看出,线程池中的真实执行的顺序是 核心线程>工作队列>最大线程数线程


缓冲队列的使用方式

常用的线程池队列有几种,SynchronousQueue、LinkedBlockingQueue 和ArrayBlockingQueue,下面我们来一一了解他们的具体作用

1.SynchronousQueue 是一种无缓冲的等待队列,它别之处在于内部没有容器,一个任务被put后。就要等待被take掉才能继续消费,这样的过程也有人说是一种配对过程。newCachedThreadPool这种预设线程池对象就用的这种队列。

2.LinkedBlockingQueue 是一种无界缓存等待队列,内部由单链表实现。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。需要注意的是,虽然这个队列通常称其为一个无界队列,但是可以人为的指定队列的大小,而且用于记录队列大小的参数字段未int,所以即使不指定队列的大小,队列的最大值也为Integer.MAX_VALUE。newFixedThreadPool这种预设线程池对象就用的这种队列。

3.ArrayBlockingQueue 是一个邮件缓冲等待队列,他是一个基于数组的阻塞队列。可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入,会根据指定的策略执行。


线程池的理解还需要结合比较多的源码阅读,让我们慢慢深入理解把,相信这些内容已经足够你驾驭线程池了!