Java线程之线程池(Thread Pool)

一、简介

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

优点:

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

缺点:

  • 频繁的线程创建和销毁会占用更多的CPU和内存,对GC产生比较大的压力。
  • 线程太多,线程切换带来的开销将不可忽视。同时线程太少,多核CPU得不到充分利用,是一种浪费。

但是要做到合理的利用线程池,帮助我们管理线程而获取并发性的工具。

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

二、案例

public class Test {
     public static void main(String[] args) {   
         ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                 new ArrayBlockingQueue<Runnable>(5));
          
         for (int i = 0; i < 15; i++) {
             MyTask myTask = new MyTask(i);
             executor.execute(myTask);
             System.out.println("线程池中线程数目:" + executor.getPoolSize() 
                     + ",队列中等待执行的任务数目:" + executor.getQueue().size()
                     + ",已执行玩别的任务数目:" + executor.getCompletedTaskCount());
         }
         executor.shutdown();
     }
}

class MyTask implements Runnable {
    
    private int taskNum;
     
    public MyTask(int num) {
        this.taskNum = num;
    }
     
    @Override
    public void run() {
        System.out.println("正在执行task " + taskNum);
        try {
            Thread.currentThread().sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task " + taskNum + "执行完毕");
    }
}

执行结果:

正在执行task 0
线程池中线程数目:1,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 1
线程池中线程数目:3,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 2
线程池中线程数目:4,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 3
线程池中线程数目:5,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 4
线程池中线程数目:5,队列中等待执行的任务数目:1,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:2,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:3,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:4,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
线程池中线程数目:6,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 10
线程池中线程数目:7,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 11
线程池中线程数目:8,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 12
线程池中线程数目:9,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 13
线程池中线程数目:10,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 14
task 3执行完毕
task 0执行完毕
task 2执行完毕
task 1执行完毕
正在执行task 8
正在执行task 7
正在执行task 6
正在执行task 5
task 4执行完毕
task 10执行完毕
task 11执行完毕
task 13执行完毕
task 12执行完毕
正在执行task 9
task 14执行完毕
task 8执行完毕
task 5执行完毕
task 7执行完毕
task 6执行完毕
task 9执行完毕

从执行结果可以看出,当线程池中线程的数目大于5时,便将任务放入任务缓存队列里面,当任务缓存队列满了之后,便创建新的线程。如果上面程序中,将for循环中改成执行20个任务,就会抛出任务拒绝异常了。

三、核心设计

Java中的线程池核心实现类是ThreadPoolExecutor,基于JDK 1.8的源码来分析Java线程池的核心设计与实现。我们首先来看一下ThreadPoolExecutorUML类图,了解下ThreadPoolExecutor的继承关系。

ThreadPoolExecutor.png

ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。

ExecutorService接口增加了一些能力:

(1). 扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;
(2). 提供了管控线程池的方法,比如停止线程池的运行。

AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?其运行机制如下图所示:

9682b17bc260cade4815c8a8dd6be54d.webp

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。

线程池的运行主要分成两部分:任务管理、线程管理。

  1. 任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:

    (1). 直接申请线程执行该任务;
    (2). 缓冲到队列中等待线程执行;
    (3). 拒绝该任务。

  2. 线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

四、实现原理

4.1 创建线程池

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

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), 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.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

ThreadPoolExecutor有四个构造方法,其中前三个最终都是调用最后一个,创建一个线程池需要输入一下几个参数:

  1. corePoolSize(核心线程数):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  2. maximumPoolSize(最大线程数):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。核心线程数和最大线程数相同,意味着该线程池中的线程都是核心线程,空闲线程就不会被销毁。值得注意的是如果使用了无界的任务队列这个参数就没什么效果
  3. keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  4. TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS,千分之一毫秒)和毫微秒(NANOSECONDS,千分之一微秒)。
  5. runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  • PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
  1. ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常有帮助。
  2. RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK 1.5提供的四种策略:
  • AbortPolicy:直接抛出异常。
  • CallerRunsPolicy:只用调用者所在线程来运行任务。
  • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  • DiscardPolicy:不处理,丢弃掉。

当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

4.2 生命周期管理

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量(workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量(workerCount)两个关键参数的维护放在了一起,如下代码所示:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // =29
private static final int CAPACITY   = (1 << COUNT_BITS) - 1; // =000 11111...

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS; // 111 00000...
private static final int SHUTDOWN   =  0 << COUNT_BITS; // 000 00000...
private static final int STOP       =  1 << COUNT_BITS; // 001 00000...
private static final int TIDYING    =  2 << COUNT_BITS; // 010 00000...
private static final int TERMINATED =  3 << COUNT_BITS; // 011 00000...

// 线程池的运行状态
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// 线程池中工作线程的数量
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 计算ctl的值,等于运行状态“加上”线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; }

ctl这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段,它同时包含两部分的信息:线程池的运行状态(runState)和线程池内有效线程的数量(workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。

ThreadPoolExecutor的状态有5种,分别为:

  • RUNNING,表示可接受新任务,且可执行队列中的任务;
  • SHUTDOWN,表示不接受新任务,但可执行队列中的任务;
  • STOP,表示不接受新任务,且不再执行队列中的任务,且中断正在执行的任务;
  • TIDYING,所有任务已经中止,且工作线程数量为0,最后变迁到这个状态的线程将要执行terminated()钩子方法,只会有一个线程执行这个方法;
  • TERMINATED,中止状态,已经执行完terminated()钩子方法;

生命周期转换如下:

20201015160250384.png

  1. 新建线程池时,它的初始状态为RUNNING,这个在上面定义ctl的时候可以看到;
  2. RUNNING -> SHUTDOWN,执行shutdown()方法时;
  3. RUNNING -> STOP,执行shutdownNow()方法时;
  4. SHUTDOWN -> STOP,执行shutdownNow()方法时;
  5. STOP -> TIDYING,执行了shutdown()或者shutdownNow()后,所有任务已中止,且工作线程数量为0时,此时会执行terminated()方法;
  6. TIDYING -> TERMINATED,执行完terminated()方法后;

源码如下:

(1). RUNNING

RUNNING,比较简单,创建线程池的时候就会初始化ctl,而ctl初始化为RUNNING状态,所以线程池的初始状态就为RUNNING状态。

// 初始状态为RUNNING
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

(2). SHUTDOWN

执行shutdown()方法时把状态修改为SHUTDOWN,这里肯定会成功,因为advanceRunState()方法中是个自旋,不成功不会退出。

public void shutdown() {
    // 上锁确保只有一个线程执行此操作
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 检查是否有权限关闭线程池以和中断线程
        checkShutdownAccess();
        // 将线程池状态设置为SHUTDOWN
        advanceRunState(SHUTDOWN);
        // 中断所有空闲线程
        interruptIdleWorkers();
        // 用于取消延时任务
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    // 将线程池置为TERMINATED状态
    tryTerminate();
}

private void checkShutdownAccess() {
    // 检查是否有安全管理器,确保调用者有权关闭线程
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkPermission(shutdownPerm);
        // 上锁确保只有一个线程执行此操作
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 对工作的线程做安全权限检查
            for (Worker w : workers)
                security.checkAccess(w.thread);
        } finally {
            mainLock.unlock();
        }
    }
}

private void advanceRunState(int targetState) {
    // assert targetState == SHUTDOWN || targetState == STOP;
    // CAS自旋操作,死循环将线程池运行状态设置为目标值
    for (;;) {
        // 获取AtomicInteger类型的控制变量ctl 
        int c = ctl.get();
        // 判断当前线程池的状态值是否大于目标值
        if (runStateAtLeast(c, targetState) ||
            // 将线程池运行状态设置为目标值,成功的话会返回true,失败则返回false
            ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
            break;
    }
}

// 中断所有空闲线程
private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

// 如果参数为false,中断所有空闲线程,如果为true,则只中断一个空闲线程
private void interruptIdleWorkers(boolean onlyOne) {
    // 上锁确保只有一个线程执行此操作
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 检查所有的工作线程是否被中断
        for (Worker w : workers) {
            Thread t = w.thread;
            // 如果没有被中断,并且工作线程获得了锁,则执行中断方法,并释放线程锁
            // 成功则返回true,表示处于无锁状态的worker(state为0)为空闲线程
            // 失败则返回false,表示处于锁定状态的worker(state为1)为工作线程,
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            // 如果为true,表示只中断一个空闲线程,退出循环,适用于用tryTerminate()方法
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

(3). STOP

执行shutdownNow()方法时,会把线程池状态修改为STOP状态,同时标记所有线程为中断状态。

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    // 上锁确保只有一个线程执行此操作
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 检查是否有权限关闭线程池以和中断线程
        checkShutdownAccess();
      	// 将线程池运行状态置为STOP
        advanceRunState(STOP);
      	// 中断所有线程
        interruptWorkers();
      	// 将未执行的任务移入列表中
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    // 将线程池置为TERMINATED状态
    tryTerminate();
    return tasks;
}

至于线程是否响应中断其实是在队列的take()poll()方法中响应的,最后会到AQS中,它们检测到线程中断了会抛出一个InterruptedException异常,然后getTask()中捕获这个异常,并且在下一次的自旋时退出当前线程并减少工作线程的数量。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 如果状态为STOP了,这里会直接退出循环,且减少工作线程数量
        // 退出循环了也就相当于这个线程的生命周期结束了
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 真正响应中断是在poll()方法或者take()方法中
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            // 这里捕获中断异常
            timedOut = false;
        }
    }
}

这里有一个问题,就是已经通过getTask()取出来且返回的任务怎么办?

实际上它们会正常执行完毕,有兴趣的同学可以自己看看runWorker()这个方法,我们下面会分析这个方法。

(4). TIDYING

当执行shutdown()shutdownNow()之后,如果所有任务已中止,且工作线程数量为0,就会进入这个状态。

// 每个工作线程的终结都会调用tryTerminate()方法
final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        // 判断线程池的状态,下面几种情况则直接返回
        // 1. 线程池处于RUNNING状态
        // 2. 线程池处于TIDYING或者TERMINATED状态,意味着已经走到了下面的步骤,线程池即将终结
        // 3. 线程池处于SHUTDOWN状态且任务队列不为空
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
        // 到这里说明当前线程池除了线程数不为0,其他条件都已经满足关闭要求
        // 如果工作线程数量不为0,则尝试中断工作线程集合中的一个空闲线程
        if (workerCountOf(c) != 0) {
            // 尝试中断空闲的线程
            interruptIdleWorkers(ONLY_ONE);
            return;
        }

        // 到这里说明线程池满足所有关闭条件的要求,接下来将线程池运行状态置为TERMINATED
        // 上锁确保只有一个线程执行此操作
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // CAS设置线程池运行状态为TIDYING
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 执行terminated()钩子方法,用于终止线程池
                    terminated();
                } finally {
                    // 将线程池运行状态置为TERMINATED,且线程数为0
                    ctl.set(ctlOf(TERMINATED, 0));
                    // 唤醒阻塞在termination条件的所有线程		
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}

实际更新状态为TIDYINGTERMINATED状态的代码都在tryTerminate()方法中,实际上tryTerminated()方法在很多地方都有调用,比如shutdown()shutdownNow()、线程退出时,所以说几乎每个线程最后消亡的时候都会调用tryTerminate()方法,但最后只会有一个线程真正执行到修改状态为TIDYING的地方。

(5). TERMINATED

修改状态为TIDYING后执行terminated()钩子方法,线程池已终止,修改状态为TERMINATED

4.3 任务执行机制

4.3.1 任务调度

线程池的主要工作流程如下图:

a76f36b95a588c0e1d224f5b83bdc94d.webp

从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:

  • 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  • 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  • 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  • 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  • 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务,默认的处理方式是直接抛异常。

上面的流程分析让我们很直观的了解的线程池的工作原理,让我们再通过源代码来看看是如何实现的。线程池执行任务的方法如下:

public void execute(Runnable command) {

    // (1).如果任务为null,则抛出NPE异常
    if (command == null)
        throw new NullPointerException();

    // (2).获取当前线程池的状态+线程个数变量的组合值
    int c = ctl.get();

    // (3).当前线程池线程个数是否小于corePoolSize,小于则开启新线程运行
    if (workerCountOf(c) < corePoolSize) {
        // 添加任务到worker集合中
        if (addWorker(command, true))
            // 成功返回
            return;
        // 失败则再次获取线程池的状态
        c = ctl.get();
    }

    // (4).如果线程池处于RUNNING状态,则添加任务到阻塞队列
    if (isRunning(c) && workQueue.offer(command)) {

        // (4.1).二次检查
        int recheck = ctl.get();
        // (4.2).如果当前线程池状态不是RUNNING则从队列删除任务,并执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);

        // (4.3).否者如果当前线程池线程空,则添加一个线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // (5).如果队列满了,则新增线程,新增失败则执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}

execute和submit方法区别

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法的话,如果在timeout时间内任务还没有执行完,就会抛出java.util.concurrent.TimeoutException

4.3.2 任务缓冲

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

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

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

adc0dcde11778fc1a2bb60c0fcfcbfa4.webp

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

974fef22f013d269778bbe7b7f81b60d-7478594d.webp

4.3.3 任务申请

由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:

7e0fe8f584b6e7d810e330500310914f.webp

getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。

4.3.4 任务拒绝

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

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

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

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

f0b32b9577575252ea4e8f2c4d39a266.webp

4.4 Worker线程管理

4.4.1 Worker线程

线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker。我们来看一下它的部分代码:

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable {

  final Thread thread;//Worker持有的线程

  Runnable firstTask;//初始化的任务,可以为null

}

Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTaskthread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

Worker执行任务的模型如下图所示:

0218c6f1b3183e3e8cfe4787a5530063.webp

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。

Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。

  • lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
  • 如果正在执行任务,则不应该中断线程。
  • 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
  • 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

在线程回收过程中就使用到了这种特性,回收过程如下图所示:

185f73b615d010ac1e8ea3c871dfc5f0.webp

4.4.2 Worker线程增加

增加线程是通过线程池中的addWorker方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTaskcorefirstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSizefalse表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示:

34512ccdefe56bb5afd365074b2eb5e2.webp

4.4.3 Worker线程回收

线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

try {
    while (task != null || (task = getTask()) != null) {
        //执行任务
    }
} finally {
    processWorkerExit(w, completedAbruptly);//获取不到任务时,主动回收自己
}

线程回收的工作是在processWorkerExit方法完成的。

cfbf1a30ecf6398b3649d7945c4f1589.webp

事实上,在这个方法中,将线程引用移出线程池就已经结束了线程销毁的部分。但由于引起线程销毁的可能性有很多,线程池还要判断是什么引发了这次销毁,是否要改变线程池的现阶段状态,是否要根据新状态,重新分配线程。

4.4.4 Worker线程执行任务

Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:

  • while循环不断地通过getTask()方法获取任务。
  • getTask()方法从阻塞队列中取任务。
  • 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
  • 执行任务。
  • 如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。

执行流程如下图所示:

2e6037b749069e684f41ce6e15da474b.webp

4.5 关闭线程池

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

只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow

五、配置线程池

5.1 合理配置线程数

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

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

任务性质不同的任务可以用不同规模的线程池分开处理。

  • CPU密集型任务配置尽可能少的线程数量,如配置Ncpu + 1个线程的线程池。
  • IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,可配置2*Ncpu10*Ncpu不等(视任务情况而定)。具体参考以下公式:
    CPU核心数 * CPU利用率 * (1 + 任务等待时间 / 计算时间)
    
  • 混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

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

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

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

建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。

//IO
public static final ExecutorService IO_EXECUTOR = new ThreadPoolExecutor(
    2, 
    128, 
    15, 
    TimeUnit.SECONDS, 
    new SynchronousQueue<>(), 
    new CustomThreadFactory("MDove-IO", CustomThreadPriority.NORMAL),
    AbortPolicy() // 根据业务情况,自行定义拒绝实现。比如上报监控平台
);

//CPU
public static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
public static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int CPU_CORE_POOL_SIZE = Math.max(Math.min(MAXIMUM_POOL_SIZE, 4),
        Math.min(CPU_COUNT + 1, 9));
 
public static final ExecutorService CPU_EXECUTOR = new ThreadPoolExecutor(
    CPU_CORE_POOL_SIZE,
    CPU_COUNT * 2 + 1,
    30, 
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(256),
    new SSThreadFactory("MDove-CPU", CustomThreadPriority.NORMAL),
    AbortPolicy() // 根据业务情况,自行定义拒绝实现。比如上报监控平台
);

5.2 动态配置线程数

动态化线程池的核心设计包括以下三个方面:

  1. 简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSizemaximumPoolSizeworkQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,Less is More。
  2. 参数可动态修改:为了解决参数不好配,修改参数成本高等问题。在Java线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。
  3. 增加线程池监控:对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。

JDK允许线程池使用方通过ThreadPoolExecutor的实例来动态设置线程池的核心策略,以setCorePoolSize为方法例,在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idleworker线程发起中断请求以实现回收,多余的worker在下次idle的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务,setCorePoolSize具体流程如下:

setCorePoolSize

六、线程池的监控

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

线程池里有一些属性在监控线程池的时候可以使用

  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount
  • largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
  • getActiveCount:获取活动的线程数。

6.2 通过扩展线程池进行监控

通过继承线程池并重写线程池的beforeExecuteafterExecuteterminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:

protected void beforeExecute(Thread t, Runnable r) { }

七、拓展

7.1 如何保证核心线程不被销毁

runWorker方法中的while循环里不断调用getTask获取新任务,getTask方法中通过blockQueue的take方法来获取队列中的任务,如果队列是空,则一直阻塞当前线程。

具体参考核心线程不被摧毁的原理

参考文章

posted @ 2022-04-25 15:20  夏尔_717  阅读(805)  评论(0编辑  收藏  举报