Java线程池ThreadPoolExecutor(一)

181 阅读7分钟

Java中几乎所有需要异步或并发执行任务的程序,都需要使用线程池。这有3个好处:

  • 降低资源消耗,通过重复利用已有线程,来降低线程创建、销毁造成的消耗;
  • 提供响应速度,当任务到达时,不用等待线程创建就能立即执行;
  • 提高对线程的管控,避免无限制的创建,实现统一分配、监控和调优。

今天,我们一起来看看Java中线程池的实现。

ThreadPoolExecutor

Java中线程池实现是java.util.concurrent.ThreadPoolExecutor类。顶层接口Executor是@FunctionalInterface的,仅有一个方法void execute(Runnable command)image.png

线程池处理任务的流程大体如下: image.png

  1. 起初线程池中没有线程,当一个任务提交给线程池后,会创建一个新线程来执行任务;
  2. 当运行中线程数到达corePoolSize时,继续的提交任务会被加入workQueue队列排队;
  3. 如果队列有界且已经放满时,将创建新线程来救急;
  4. 如果线程数到达maximumPoolSize的,却仍有新任务,这时会执行拒绝策略;
  5. 超过corePoolSize的救急线程,如果在一定时间没有获得任务(即空闲),将会被回收。空闲时间由keepAliveTime 和 unit 来控制。

1 线程池状态表示

ThreadPoolExecutor中,使用一个AtomicInteger属性(称为ctl))的高3位来表示线程池状态(即runState),低29位表示线程数量(即workerCount),最大线程数为(2^29)-1 (约500万多)。使用CAS原子操作更新ctl。

线程池初始状态为RUNNING,workerCount=0。 image.png 注意,RUNNING时最高位为1,即负值。因此从数字上来比较:TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING

状态名高3位接收新任务处理队列任务
RUNNING111YY
SHUTDOWN000NY
STOP001NN
TIDYING010NN
TERMINATED011NN

2 构造方法

ThreadPoolExecutor类主要属性如下:

// 任务队列
private final BlockingQueue<Runnable> workQueue;
// 主要用于操作workers时加锁
private final ReentrantLock mainLock = new ReentrantLock();
// worker集合,非线程安全的,因此需要mainLock
private final HashSet<Worker> workers = new HashSet<Worker>();
// 线程池曾创建线程的最大数
private int largestPoolSize;
// 完成任务的计数器。仅在工作线程终止时更新
private long completedTaskCount;

// 线程工厂
private volatile ThreadFactory threadFactory;
// 任务拒绝策略
private volatile RejectedExecutionHandler handler;
// 空闲线程最大存活时间,在线程数量超过corePoolSize或allowCoreThreadTimeOut=true时起作用
private volatile long keepAliveTime;
// 核心线程是否空闲超时,默认为false
private volatile boolean allowCoreThreadTimeOut;

// 核心线程数
private volatile int corePoolSize;
// 最大线程数
private volatile int maximumPoolSize;

全参构造方法如下:

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

3 ThreadFactory

一个按需创建新线程的工厂类,消除了对new Thread()调用的硬依赖。

public interface ThreadFactory {
    // 可以为线程设置更多属性,如priority, name, daemon, ThreadGroup等.
    Thread newThread(Runnable r);
}

Executors类中提供了一个实现DefaultThreadFactory:创建普通优先级、非守护线程。 image.png 创建ThreadPoolExecutor时如果不提供threadFactory,那么默认使用DefaultThreadFactoryimage.png image.png

4 拒绝策略

一个处理器,用来处理ThreadPoolExecutor不能执行的任务。

public interface RejectedExecutionHandler {
    // 当一个任务被提交时,却没有更多可用线程或任务队列已满,将执行该方法。
    // 可能会抛出RejectedExecutionException
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

ThreadPoolExecutor中提供了4 种拒绝策略实现,我们也可自行扩展:

  • AbortPolicy:让调用者抛出 RejectedExecutionException 异常,这是默认策略;
  • CallerRunsPolicy:让调用者运行任务
  • DiscardPolicy:丢弃本次任务
  • DiscardOldestPolicy:放弃队列中最早的任务,提交该任务。 image.png image.png

来看一下DiscardOldestPolicy实现:丢弃队列头任务,提交当前任务。

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
	public DiscardOldestPolicy() { }

	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
		if (!e.isShutdown()) {
                  // 忽略返回值,即丢弃队列头任务
			e.getQueue().poll();
			// 提交当前任务
			e.execute(r);
		}
	}
}

5 工作流程

ThreadPoolExecutor.execute()方法主要流程如图所示。 image.png

ThreadPoolExecutor.execute()方法中,反复对ctl进行判断,因为可以并行提交任务,且线程池状态也随时可能发生变化。

  public void execute(Runnable command) {
    if (command == null)
      throw new NullPointerException();
    int c = ctl.get();
    // 线程数小于corePoolSize
    if (workerCountOf(c) < corePoolSize) {
      // 创建核心线程,可能会失败
      if (addWorker(command, true))
        return;
      c = ctl.get();
    }
    // 线程数大于或等于corePoolSize时,尝试将任务放入队列;
    if (isRunning(c) && workQueue.offer(command)) {
      int recheck = ctl.get();
      // 线程池非Running状态时,从队列移除该任务
      if (!isRunning(recheck) && remove(command))
        // 执行拒绝策略
        reject(command);
        // 线程池Running状态,但任务未放入队列中时
      else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
    }
    // 将任务放入队列失败,创建救急线程
    else if (!addWorker(command, false))
      reject(command);
  }

6 工作线程Worker

线程池创建线程时,会封装成工作线程Worker,Worker执行完firstTask后,会循环获取任务队列中任务来执行。

// Worker类部分代码
// 实现了AQS,自身就可以lock/unlock,不借助于ReentrantLock。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {

  //关联的线程
  final Thread thread;
  // 创建work时的首个任务(不是从队列中获取的)
  Runnable firstTask;
  // 以完成任务数
  volatile long completedTasks;

  Worker(Runnable firstTask) {
    // 防止创建后、执行runWorker前,被lock住。
    setState(-1);
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
  }

  // 循环获取任务执行
  public void run() {
    runWorker(this);
  }
}

再来看ThreadPoolExecutor.runWorker()方法逻辑:当执行完firstTask后,继续从队列getTask()来执行;如果未获取到任务,则退出且销毁线程。

  final void runWorker(Worker w) {
    // 提交任务的线程
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock();
    // 记录worker是否非正常退出
    boolean completedAbruptly = true;
    try {
      // firstTask或从队列拉取任务
      while (task != null || (task = getTask()) != null) {
        w.lock();

        try {
          // 默认空实现,可扩展
          beforeExecute(wt, task);
          Throwable thrown = null;
          try {
            // 执行提交的任务
            task.run();
          } catch (Throwable x) {
            thrown = x;
            throw new Error(x);
          } finally {
            // 默认空实现,可扩展
            afterExecute(task, thrown);
          }
        } finally {
          task = null;
          w.completedTasks++;
          w.unlock();
        }
      }
      completedAbruptly = false;
    } finally {
      // worker退出
      processWorkerExit(w, completedAbruptly);
    }
  }

6.1 Worker何时被启动的?

ThreadPoolExecutor.execute()中多出调用了addWorker(),后者在worker创建成功后就立即启动了它。

image.png

  • Worker实现了Runnable接口;
  • 创建Worker.thread时,用new Thread(worker);
  • thread启动后,线程任务就是Worker.run();
  • Worker.run()仅仅是调用ThreadPoolExecutor.runWorker() image.png

6.2 何时会从队列中获取不到任务?

ThreadPoolExecutor#getTask方法中,使用for (;;)死循环,直到从队列中获取一个任务或者返回null时才return。以下情况时将获取不到任务:

  • 线程池状态不满足时返回null:非running状态,或者非running、SHUTDOWN状态且队列为空时 image.png
  • 触发线程数限制或空闲超时时返回null: image.png

6.3 非核心线程的空闲超时如何实现?

  • 当有空闲超时限制时,在从队列获取任务时,使用阻塞带超时的poll(long timeout, TimeUnit unit)方法,超时后将返回null,进而worker退出;
  • 而不受超时限制时,使用阻塞无超时的take()方法,直到从队列获取到一个任务时才继续执行。
    // 限制空闲线程数否
    boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    try {
    
      Runnable r = timed ?
          workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
          workQueue.take();
      if (r != null)
        return r;
      timedOut = true;
    } catch (InterruptedException retry) {
      timedOut = false;
    }

6.4 核心线程与非核心线程有无标识?

结论:线程池中某个线程被创建后,并非固定就是核心的或非核心的。

  • 首先,Worker类中没有一个属性来标识线程核心与否。
  • 其次,addWorker()方法中,仅根据core来区分当前线程数与corePoolSize还是maximumPoolSize来比较。
private boolean addWorker(Runnable firstTask, boolean core) {
    for (; ; ) {
      int wc = workerCountOf(c);
      if (wc >= CAPACITY ||
          wc >= (core ? corePoolSize : maximumPoolSize)) // core仅用于此处判断
        return false;
      // ......
    }
  • 最后,在getTask()方法中,只要当前线程数大于corePoolSize,当前线程都有可能被淘汰。 image.png

6.5 任务执行异常时补充新worker

processWorkerExit()方法中,将从ThreadPoolExecutor.workers中移除当前worker,线程也被销毁。对于completedAbruptly参数:

  • 由于没有获取到可执行任务时,completedAbruptly = false,即正常退出。此时,如果当前线程数不大于核心线程数,且任务队列不为空时,将补充一个新worker;否则继续worker退出。
  • 当因为执行任务发生异常时,completedAbruptly=true,将补充一个新worker。 image.png 由此可知,线程池中线程因执行任务异常而意外退出时,将有机会补充一个新线程。

6.6 预创建核心线程

起初线程池中无任何线程,当新任务到达且线程数小于corePoolSize时,才去创建并启动一个核心线程,来执行该任务。这在响应时间上有损耗。

ThreadPoolExecutor提供了两个方法,可以在创建线程池后,立即启动核心线程,即使没有任务达到——即预热

  • prestartCoreThread():启动一个核心线程 image.png
  • prestartAllCoreThreads():启动所有核心线程 image.png