Java 中的线程池

123 阅读6分钟

在开发中,合理地使用线程池能带来3个好处:
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行。
第三:提高线程的可管理性。线程时稀缺资源,如果无限制地创建,不仅回消耗系统资源,还会降低系统地稳定性,使用线程池可以进行统一分配、调优和监控。

线程池地实现原理

图9-1 线程池的主要处理流程.png 从上图可以看出,当提交一个新的任务到线程池时,线程池的处理流程如下

  1. 线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
  2. 线程池判断工作队列是否已满。如果工作队列没有满,则将新提交的任务存储在这个工作队列。如果工作队列满了,则进入下个流程。
  3. 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和粗略来处理这个任务。
    ThreadPoolExecutor 执行 execute() 方法的示意图如下 图9-2 ThreadPoolExecutor 执行示意图.png
  4. 如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意,执行这一步需要获取全局锁)
  5. 如果运行的线程等于或多余 corePoolSize,则将任务加入 BlockingQueue
  6. 如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步需要获取全局锁)
  7. 如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution() 方法
    ThreadPoolExecutor 采用上述步骤的总体设计思路,是为了在执行 execut() 方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在 ThreadPoolExecutor 完成预热之后(当前运行地线程数大于等于 corePoolSize),几乎所有地 execute() 方法调用都是执行步骤2,而步骤2不需要获取全局锁。
    源码分析(Android API 29):
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    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);
}

线程池地使用

线程池的创建

可以通过 ThreadPoolExecutor 来创建一个线程池

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
  1. corePoolSize (线程池地基本大小):如果调用了线程池地 prestartAllCoreThreads() 方法,线程池会提前创建并启动所有基本线程
  2. workQueue (任务队列):用于保存等待执行任务地阻塞队列。可以选择以下几个阻塞队列。
    • ArrayBlockingQueue:一个基于数组结构地有界阻塞队列,此队列按 FIFO (先进先出)原则对元素进行排序
    • LinkedBlockingQueue:一个基于链表结构地阻塞队列,按照 FIFO 排序元素,吞吐量通常要高于 ArrayBlockingQueue。Executors.newFixedThreadPool() 使用了这个队列
    • SynchronousQueue:一个不存储元素地阻塞队列。每个插入操作必须等待另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常高于 LinkedBlockingQueue。Executors.newCachedThreadPool() 使用了这个队列
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列
  3. maximumPoolSize (线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
  4. ThreadFactory:用于设置创建线程的工厂
  5. RejecteExecutionHandler (饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表示无法处理新任务时抛出异常。在 JDK1.5 中Java线程池框架提供了4种策略。
    • AbortPolicy:直接抛出异常
    • CallerRunsPolicy:只用调用者所在线程来运行任务
    • DiscardOlderstPolicy:丢弃队列里最近的一个任务,并执行当前任务
    • DiscardPolicy:不处理,丢弃掉
  6. keepAliveTime(线程活动保持时间):如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率
  7. TimeUnit(线程活动保持时间的单位)

向线程池提交任务

可以使用两个方法向线程池提交任务,分别是 execute() 和 submit() 方法。
execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit() 方法用于提交需要返回值的任务。线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get 方法来获取返回值,get 方法会阻塞当前线程直到任务完成。

关闭线程池

可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池种的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow 先将线程池的状态设置为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状态设置成 SHUTDOWN,然后中断所有没有正在执行任务的线程。

合理地配置线程池

要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度分析。

  • 任务的性质:CPU密集型任务、IO密集型任务和混合型任务
  • 任务的优先级:高、中和低
  • 任务的执行时间:长、中和短
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接 CPU密集型任务应配置尽可能小的线程,如配置 CPU数量 + 1 个线程的线程池。由于 IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 2 * CPU数量。可以通过 Runtime.getRuntime().availableProcessors() 方法获得当前设备的CPU个数。

线程池的监控

可以通过线程池提供的参数进行监控

  • taskCount:线程池需要执行的任务数量
  • completedTaskCount:线程池在运行过程中已完成的任务数
  • largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过
  • getPoolSize:线程池的线程数量
  • getActiveCount:获取活动的线程数