2

1、为什么使用线程池

  1. 降低资源消耗
    通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  2. 提高响应速度
    当任务到达时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性
    使用线程池可以对线程进行统一分配、 管理和监控

3、创建线程池

使用ThreadPoolExecutor类创建线程池,它的构造方法一共有7个参数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

corePoolSize: 核心线程数,线程池的在空闲情况下所能够保持的线程数。

当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

maximumPoolSize: 线程池所能保存的最大线程数量

如果阻塞队列满了并且线程池已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。如果使用了无界的任务队列,则该参数无效

keepAliveTime: 线程的生存时间

当线程数大于corePoolSize时,多余的空闲线程具有的生存时间,当线程在一定时间内处于空闲状态(没有执行任务),线程池会自动销毁该线程,但线程池不会销毁所有线程,而是保存最多corePoolSize个线程。

TimeUnit: 线程生存时间的单位

BlockingQueue: 任务队列

用于保存等待执行的任务的阻塞队列。可选队列类型如下:

  • ArrayBlockingQueue:基于数组的有界阻塞队列,此队列按FIFO(先进先出)原
    则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表的阻塞队列,如果创建时设置了size参数就是有界队列,否则是无界队列。此队列按FIFO排序元素,吞吐量通常要高于数组队列。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue:存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked- BlockingQueue,静态工厂方法Executors.newCachedThreadPool()使用了这个队列。
  • PriorityBlockingQueue: 具有优先级的无限阻塞队列。

ThreadFactory: 线程工厂

用于创建线程的工厂,可以在创建方法可以自定义线程的基本属性(如:名称、优先级、设置守护线程)

// 使用开源框架guava提供的ThreadFactoryBuilder创建线程工厂
new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build() ;

RejectedExecutionHandler: 饱和策略

当阻塞队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。在JDK中提供了以下4种策略:

  • AbortPolicy:直接抛出异常(默认)
  • DiscardPolicy:直接丢弃当前任务
  • DiscardOldestPolicy:丢弃队列里最近的任务,并执行当前任务。
  • CallerRunsPolicy:只用调用者所在线程来运行任务。

3、线程池工作流程

在这里插入图片描述
0)使用者向线程池提交了一个任务

1)如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务(创建线程需要获取全局锁)。

2)如果运行的线程数大于等于corePoolSize,则将任务加入阻塞队列

3)如果阻塞队列已满,则创建新的线程来处理任务

4)如果创建新线程将使当前运行的线程超出最大线程数,则采取饱和策略处理

线程池采用上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁。在线程池完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

4、线程池的生命周期

在这里插入图片描述

调用线程池的shutdown()shutdownNow()方法关闭线程池。

它们的原理是:遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以不能终止无法响应中断的任务。

shutdownNow:首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

shutdown:只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

在这里插入图片描述

5、线程池配置

要想合理地配置线程池,就必须首先分析任务特性,性质不同的任务用不同规模的线程池分开处理。可以从以下几个角度来分析:

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

通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数

CPU密集型任务应配置尽可能小的线程,如配置N+1个线程的线程池
IO密集型任务的线程不是一直在执行任务(被IO阻塞),应配置尽可能多的线程,如:2*Ncpu

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。注意如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行

执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。

6、线程复用原理

线程池有一个内部类叫 Worker,它实现了 Runnable 接口,它有2个重要的内部变量 thread (创建的线程) 和 firstTask (线程的首任务)

Worker 的构造方法接收一个 Runnable 参数,将其赋值给 firstTask,还会使用线程池的线程工厂的 newThread(Runnable task) 方法创建线程,将自身作为参数传入。这样一来,当创建的线程执行时,就会执行 Worker对象 的 run() 方法,实际就是执行 runWorker() 方法。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
    // 创建的线程
    final Thread thread;
    // 线程执行的第一个任务
    Runnable firstTask;
    
    Worker(Runnable firstTask) {
        setState(-1); 
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() {
        runWorker(this);
    }

}

runWorker() 中,当 Worker 对象的 firstTask 没有执行时,会执行它的 firstTask;若已执行,则会从阻塞队列中获取任务执行,这个过程会持续循环直到线程停止为止。至此就实现了线程的复用。

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        
        try {
            while (task != null || (task = getTask()) != null) {
                ...
                task.run();
                ...
            }
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

线程池调用execute方法提交任务,通过addWorker()创建 Worker 对象,并启动新建的线程

public void execute(Runnable command) {
    // 如果当前线程数小于核心线程数,则创建新线程
    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);
}

private boolean addWorker(Runnable firstTask, boolean core) {
    .......
    Worker w = null;
    try {
        // 创建 Worker
        w = new Worker(firstTask);
        // 获取 Worker 中的线程对象
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            ........
            // 运行线程
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

这是线程池execute()方法的时序图,通过时序图便于理解上面的过程
在这里插入图片描述


223RIdBk
4 声望1 粉丝