Analysis of core code of Java thread pool framework

Reprinted from  Java Thread Pool Framework Core Code Analysis

foreword

In multi-threaded programming, it is unrealistic to assign a thread to each task, and the overhead and resource consumption of thread creation are high. The thread pool came into being and became a powerful tool for us to manage threads. Through the Executor interface, Java provides a standard method to decouple the task submission process and execution process, and use Runnable to represent the task.

Next, let's analyze the implementation of the Java thread pool framework ThreadPoolExecutor.

The following analysis is based on JDK1.7

The life cycle

In ThreadPoolExecutor, the upper 3 bits of CAPACITY are used to represent the running status, which are:

  1. RUNNING: Receive new tasks and process tasks in the task queue
  2. SHUTDOWN: Do not receive new tasks, but process tasks from the task queue
  3. STOP: Do not receive new tasks, do not process the task queue, and interrupt all ongoing tasks
  4. TIDYING: All tasks have been terminated, the number of worker threads is 0, and terminated() will be executed when this state is reached
  5. TERMINATED: terminated() has finished executing

Atomic classes are used to represent status bits in ThreadPoolExecutor

1
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0 ));

thread pool model

core parameters

  • corePoolSize: The minimum number of worker threads alive (if allowCoreThreadTimeOut is set, the value is 0)
  • maximumPoolSize: the maximum number of threads, limited by CAPACITY
  • keepAliveTime: The survival time of the corresponding thread, the time unit is specified by TimeUnit
  • workQueue: A work queue that stores tasks to be executed
  • RejectExecutionHandler: Rejection policy, which will be triggered when the thread pool is full

The maximum capacity of the thread pool : the first three bits in CAPACITY are used as flag bits, which means that the maximum capacity of the worker thread is (2^29)-1

four models

  • CachedThreadPool: A cacheable thread pool. If the current size of the thread pool exceeds the processing demand, the idle threads will be recycled. When the demand increases, new threads can be added. There is no limit to the size of the thread pool.
  • FixedThreadPool:一个固定大小的线程池,提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的大小将不再变化。
  • SingleThreadPool:一个单线程的线程池,它只有一个工作线程来执行任务,可以确保按照任务在队列中的顺序来串行执行,如果这个线程异常结束将创建一个新的线程来执行任务。
  • ScheduledThreadPool:一个固定大小的线程池,并且以延迟或者定时的方式来执行任务,类似于Timer。

执行任务 execute

核心逻辑:

1. 当前线程数量 < corePoolSize,直接开启新的核心线程执行任务addWorker(command, true)
2. 当前线程数量 >= corePoolSize,且任务加入工作队列成功

  • 检查线程池当前状态是否处于RUNNING
  • 如果否,则拒绝该任务
  • 如果是,判断当前线程数量是否为 0,如果为 0,就增加一个工作线程。

3. 开启普通线程执行任务addWorker(command, false),开启失败就拒绝该任务

从上面的分析可以总结出线程池运行的四个阶段:

  1. poolSize < corePoolSize 且队列为空,此时会新建线程来处理提交的任务
  2. poolSize == corePoolSize,此时提交的任务进入工作队列,工作线程从队列中获取任务执行,此时队列不为空且未满。
  3. poolSize == corePoolSize,并且队列已满,此时也会新建线程来处理提交的任务,但是poolSize < maxPoolSize
  4. poolSize == maxPoolSize,并且队列已满,此时会触发拒绝策略

拒绝策略

前面我们提到任务无法执行会被拒绝,RejectedExecutionHandler是处理被拒绝任务的接口。下面是四种拒绝策略。

  • AbortPolicy:默认策略,终止任务,抛出RejectedException
  • CallerRunsPolicy:在调用者线程执行当前任务,不抛异常
  • DiscardPolicy: 抛弃策略,直接丢弃任务,不抛异常
  • DiscardOldersPolicy:抛弃最老的任务,执行当前任务,不抛异常

线程池中的 Worker

Worker继承了AbstractQueuedSynchronizer和Runnable,前者给Worker提供锁的功能,后者执行工作线程的主要方法runWorker(Worker w)(从任务队列捞任务执行)。Worker 引用存在workers集合里面,用mainLock守护。

1
2
private final ReentrantLock mainLock = new ReentrantLock();
private final HashSet<Worker> workers = new HashSet<Worker>();

核心函数 runWorker

下面是简化的逻辑,注意:每个工作线程的run都执行下面的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
final void runWorker(Worker w) {
     Thread wt = Thread.currentThread();
     Runnable task = w.firstTask;
     w.firstTask = null ;
     while (task != null || (task = getTask()) != null ) {
         w.lock();
         beforeExecute(wt, task);
         task.run();
         afterExecute(task, thrown);
         w.unlock();
     }
     processWorkerExit(w, completedAbruptly);
}
  1. 从getTask()中获取任务
  2. 锁住 worker
  3. 执行beforeExecute(wt, task),这是ThreadPoolExecutor提供给子类的扩展方法
  4. 运行任务,如果该worker有配置了首次任务,则先执行首次任务且只执行一次。
  5. 执行afterExecute(task, thrown);
  6. 解锁 worker
  7. 如果获取到的任务为 null,关闭 worker

获取任务 getTask

线程池内部的任务队列是一个阻塞队列,具体实现在构造时传入。

1
private final BlockingQueue<Runnable> workQueue;

getTask()从任务队列中获取任务,支持阻塞和超时等待任务,四种情况会导致返回null,让worker关闭。

  1. 现有的线程数量超过最大线程数量
  2. 线程池处于STOP状态
  3. 线程池处于SHUTDOWN状态且工作队列为空
  4. 线程等待任务超时,且线程数量超过保留线程数量

核心逻辑:根据timed在阻塞队列上超时等待或者阻塞等待任务,等待任务超时会导致工作线程被关闭。

1
2
3
4
timed = allowCoreThreadTimeOut || wc > corePoolSize;
Runnable r = timed ?
     workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
     workQueue.take();

在以下两种情况下等待任务会超时:

  1. 允许核心线程等待超时,即allowCoreThreadTimeOut(true)
  2. 当前线程是普通线程,此时wc > corePoolSize

工作队列使用的是BlockingQueue,这里就不展开了,后面再写一篇详细的分析。

总结

  • ThreadPoolExecutor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。
  • Executors提供了四种基于ThreadPoolExecutor构造线程池模型的方法,除此之外,我们还可以直接继承ThreadPoolExecutor,重写beforeExecute和afterExecute方法来定制线程池任务执行过程。
  • 使用有界队列还是无界队列需要根据具体情况考虑,工作队列的大小和线程的数量也是需要好好考虑的。
  • 拒绝策略推荐使用CallerRunsPolicy,该策略不会抛弃任务,也不会抛出异常,而是将任务回退到调用者线程中执行。

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325972536&siteId=291194637