线程池笔记(一)线程池简介与使用

1,010 阅读3分钟

线程池简介

线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。

线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

而本文描述线程池是JDK中提供的ThreadPoolExecutor类。

当然,使用线程池可以带来一系列好处:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

线程池的使用

public class Test {
    static class Task implements Runnable{
        private String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("start task: " + name);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end task: "+ name);
        }
    }

    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 6; i++){
            es.execute(new Task("task" + i));
        }
        es.shutdown();
    }
}
/*
执行结果
start task: task0
start task: task1
end task: task0
start task: task2
end task: task1
start task: task3
end task: task2
start task: task4
end task: task3
start task: task5
end task: task4
end task: task5
*/

线程池的核心参数

  • 核心线程数:核心线程会一直存活。当任务到来时核心线程才会被创建并初始化,当然也可以调用prestartCoreThread/prestartAllCoreThreads方法让线程池一开始就创建核心线程。

  • 线程池所能容纳的最大线程数:当活动线程数到达该数值后,后续的任务将会阻塞。

  • 非核心线程闲置超时时长:超过该时长,非核心线程会被回收。与下面的时间单位合并得到具体的时长。

  • 指定时间单位:毫秒、秒、分等时间单位。

  • 任务队列:通过线程池的execute()方法提交的Runnable对象,将存储在该参数中。

  • 线程工厂:为线程池创建新线程。

  • 拒绝策略:如果线程池容纳不了新的任务,如何去处理这些任务的策略。

private volatile int corePoolSize;								//核心线程数量
private int largestPoolSize;											//最大线程总数
private volatile long keepAliveTime; 							//非核心线程的超时时长
private final BlockingQueue<Runnable> workQueue;	//任务队列
private volatile ThreadFactory threadFactory;			//线程工厂
private volatile RejectedExecutionHandler handler;//拒绝策略

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;
}

常见的几种线程池

  • 定长线程池只有核心线程 & 不会被回收、线程数量固定、任务队列无大小限制(超出的线程任务会在队列中等待);应用场景:控制线程最大并发数。

  • 定时线程池核心线程数量固定、非核心线程数量无限制(闲置时马上回收);应用场景:执行定时 / 周期性 任务。

  • 可缓存线程池只有非核心线程、线程数量不固定(可无限大)、灵活回收空闲线程(具备超时机制,全部回收时几乎不占系统资源)、新建线程(无线程可用时)、任何线程任务到来都会立刻执行,不需要等待;应用场景:执行大量、耗时少的线程任务。

  • 单线程化线程池只有一个核心线程(保证所有任务按照指定顺序在一个线程中执行,不需要处理线程同步的问题)。应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作,文件操作等。

任务的处理

当向线程池提交一个新任务时,会根据线程池的状态进行一系列的判断从而决定如何执行任务。任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  1. 首先检测线程池运行状态,如果不是运行状态,则直接拒绝,线程池要保证在运行的状态下执行任务。
  2. 工作线程数<核心线程数时,则创建并启动一个线程来执行新提交的任务。
  3. 工作线程数≥核心线程数时,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 核心线程数≤工作线程数<最大线程数时,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 工作线程数≥最大线程数时,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

图4 任务调度流程

任务的缓冲

任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

下图中展示了线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素:

图5 阻塞队列

使用不同的队列可以实现不一样的任务存取策略。在这里,我们可以再介绍下阻塞队列的成员:

img

任务的拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略是一个接口,其设计如下:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

用户可以通过实现这个接口去定制拒绝策略,也可以选择JDK提供的四种已有拒绝策略,其特点如下:

img

参考资料:

  1. Android多线程:线程池ThreadPool 全面解析
  2. 深入理解 Java 线程池:ThreadPoolExecutor
  3. Java线程池实现原理及其在美团业务中的实践 - 美团技术团队