Java 线程池详解

概述

Java 中的线程池是运行场景最多的并发框架,合理使用线程池能够带来三个好处:

  • 降低资源消耗。通过重复利用已有的线程降低线程创建和销毁造成的消耗
  • 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行
  • 提高线程可管理性。线程是稀缺资源,使用线程池进行统一分配、调优和监控,可以降低资源消耗,提高系统稳定性

线程池的实现原理

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

  1. 线程池判断核心线程池里的线程是否都在执行任务,如果不是,创建一个新的工作线程执行任务,否则进入下一流程
  2. 线程池判断工作队列是否已满,如果工作队列没有满,将新提交的任务存储在工作队列中,否则进入下一流程
  3. 线程池判断线程池里的线程是否都处于工作状态,如果没有,创建一个新的工作线程执行任务,否则交给饱和策略来处理这个任务

使用线程池

1. 创建线程池

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

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);

创建一个线程需要输入几个参数,如下:

  • corePoolSize(线程池的基本大小)

    当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即时其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建

  • maximumPoolSize(线程池最大数量)

    线程池允许创建的最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用无界阻塞队列做任务队列,则这个参数没有什么效果

  • keepAliveTime(线程活动保持时间)

    线程池的工作线程空闲后,保持存活的时间。如果任务很多,并且每个任务的执行时间都比较短,可以调大时间,提高线程利用率

  • unit(线程保持活动时间的单位)

    可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微妙(MICROSECONDS)和纳秒(NANOSECONDS)

  • workQueue(任务队列)

    用于保存等到执行的任务的阻塞队列,可以选择以下几个阻塞队列:

    • ArrayBlockingQueue

      是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序

    • LinkedBlockingQueue

      一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素,吞吐量通常高于 ArrayBlockingQueue

    • SynchronousQueue

      一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一致处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue

    • PriorityBlockingQueue

      一个具有优先级的无界阻塞队列

  • threadFactory

    用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字

  • handler(饱和策略)

    当任务和线程池都满了,说明线程池处于饱和状态,必须采取一种策略处理提交的新任务。在 JDK5 中线程池框架提供了四种策略,也可以根据需要实现 RejectedExecutionHandler 接口自定义策略:

    • AbortPolicy:直接抛出异常,默认采取这种策略,一般出现异常时,可以提示稍后再试,或者把未执行的任务记录下来,等到适当时机再次执行
    • CallerRunsPolicy:使用调用者所在线程来运行任务,保证所有任务都能执行,但如果并发量过大会有风险
    • DiscardOldestPolicy:丢弃队列最老(也就是最近将要执行)的一个任务,并执行当前任务,适用于需要淘汰等待时间最长任务的场景
    • DiscardPolicy:不处理,丢弃掉,一般不使用,除非需要执行的任务不重要,丢弃了也没事,或者能把丢弃的任务几楼下来,等到适当时机再次执行

如何合理设置线程池参数,需要根据执行的任务类型来确定,一般来说任务分 CPU 密集型和 IO 密集型两种:

  • CPU 密集型:业务逻辑复杂的任务,IO 较少,几乎全程没有阻塞
  • IO 密集型:业务逻辑简单的任务,但 IO 频繁

查看机器的 CPU 核数

System.out.println(Runtime.getRuntime().availableProcessors());

对于 CPU 密集型任务,假设 CPU 核数为 N,可以设置 corePoolSize = N,因为 CPU 密集型任务对 CPU 的切换调度较少

对于 IO 密集型任务,假设 CPU 核数为 N,可以设置 corePoolSize = 2*N,因为线程在等待 IO 时会释放 CPU

maxPoolSize,最大线程数,一般设置成和 corePoolSize 一样,减少创建线程的开销

keepAliveTime,空闲线程的存活时间,超过这个时间无任务执行的线程将被终止,直到线程数回到 corePoolSize。对于 CPU 密集型任务,可以设置较短的存活时间,减少资源浪费。对于 IO 密集型任务,可以设置较长的存活时间,保持线程池的响应能力

handler,饱和策略,根据业务场景选择合适的饱和策略。如果不能丢失任务,可以选择CallerRunsPolicy。如果接受任务被丢弃,可以选择 DiscardOldestPolicy 或 DiscardPolicy

workQueue,任务队列。如果使用无界队列,当任务耗时较长时,可能会导致大量任务在队列堆积,最终导致 OOM。使用有界队列可以防止资源耗尽,容量大小应根据任务生产速率和执行速率来设定,防止队列过早填满导致线程池迅速扩张。如果希望任务不等待直接移交给工作线程,可使用 SynchronousQueue,要将一个任务放入 SynchronousQueue,必须有另一个线程正在等待接收这个元素,只有在使用无界线程池(maximumPoolSize = Integer.MAX_VALUE)或者有饱和策略时才建议使用该队列

2. 向线程池提交任务

可以使用 execute() 和 submit() 方法向线程池提交任务

  • execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功

    threadsPool.execute(new Runnable() {
        @Override
        public void run() {
            //...
        }
    })
    
  • submit() 方法用于提交需要返回值的任务,线程池会返回一个 future 对象,通过这个对象可以判断任务是否执行成功

    Future<Object> future = executor.submit(hasReturnValueTask);
    try {
        Object s = future.get();
    } catch(InterruptedException e) {
        // 处理中断异常
    } catch(ExecutionException e) {
        // 处理无法执行任务异常
    } finally {
        // 关闭线程池
        executor.shutdown();
    }
    

3. 关闭线程池

可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池,它们的原理是遍历线程池中的工作线程,逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止

shutdown 方法和 shutdownNow 方法存在一定的区别:

  • shutdownNow 方法首先将线程池状态设置成 STOP,不接收新任务,不处理已添加的任务,且会中断正在执行任务的线程
  • shutdown 方法只是将线程池状态设置成 SHUTDOWN 状态,不接收新任务,但可以处理已添加的任务,等待正在执行任务的线程结束

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


线程池原理

由线程池的使用流程,我们可以得知,向线程池提交任务的方法是 execute 方法,因此我们首先从该方法入手:

public void execute(Runnable command) {
  if (command == null)
    throw new NullPointerException();
  // ctl.get 的获取值能用来判断线程池状态和线程数
  int c = ctl.get();
  // 1.1 workerCountOf 方法用于获取线程池中线程数
  if (workerCountOf(c) < corePoolSize) {
    // 1.2 线程池中线程数小于核心线程数,尝试创建核心线程执行任务
    if (addWorker(command, true))
        return;
    c = ctl.get();
  }
  // 2.1 到此处说明线程池中线程数大于核心线程数或者创建线程失败
  // 2.1 如果线程是运行状态并且可以使用 offer 将任务加入阻塞队列
  // 2.2 offer 是非阻塞操作
  if (isRunning(c) && workQueue.offer(command)) {
    // 2.3 重新检查线程池状态,因为上次检测后线程池状态可能发生改变
    int recheck = ctl.get();
    // 2.4 如果非运行状态就移除任务并执行拒绝策略
    if (! isRunning(recheck) && remove(command))
      reject(command);
    // 2.5 如果是运行状态,并且线程数是0,则创建线程
    else if (workerCountOf(recheck) == 0)
      // 2.6 线程数是0,则创建非核心线程,且不指定首次执行任务
      // 2.7因为此时任务已经加入阻塞队列,只需要等待线程获取执行即可
      addWorker(null, false);
  }
  // 3.1 阻塞队列已满,创建非核心线程执行任务
  else if (!addWorker(command, false))
    // 3.2 如果失败,则执行拒绝策略
    reject(command);
}

接下来看 execute 方法中创建线程的 addWoker 方法,addWoker 方法承担了核心线程和非核心线程的创建,通过一个 boolean 参数 core 来区分是创建核心线程还是非核心线程

private boolean addWorker(Runnable firstTask, boolean core) {
  // 这里做了一个 retry 标记,相当于 goto
  retry:
  for (int c = ctl.get();;) {
    // 如果线程池处于关闭状态则不创建线程
    if (runStateAtLeast(c, SHUTDOWN)
        && (runStateAtLeast(c, STOP) || firstTask != null || workQueue.isEmpty()))
      return false;

      for (;;) {
        // 1.1 根据 core 来确定创建最大线程数,超过最大值则创建线程失败,
        // 1.2 注意这里的最大值可能有三个 corePoolSize、maximumPoolSize 和线程池线程的最大容量 CAPACITY
        if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
          return false;
        // 1.3 通过 CAS 将线程数 +1,如果成功则跳出循环,执行 2.1 的逻辑    
        if (compareAndIncrementWorkerCount(c))
          break retry;
        c = ctl.get();
        // 1.4 线程池的状态发生了改变,退回 retry 重新执行
        if (runStateAtLeast(c, SHUTDOWN))
          continue retry;
      }
  }
  
  boolean workerStarted = false;
  boolean workerAdded = false;
  Worker w = null;
  try {
    // 2.1 实例化一个 Worker,内部封装了线程
    w = new Worker(firstTask);
    // 2.2 取出新建的线程
    final Thread t = w.thread;
    if (t != null) {
        // 2.3 加锁保证线程安全
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
          int rs = runStateOf(ctl.get());
          // 2.4 拿到锁后重新检查线程池状态,只有处于 RUNNING(RUNNING 值小于 SHUTDOWN)
          // 或者 SHUTDOWN 并且 firstTask==null 才会创建线程
          if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
            // 2.5 线程已经启动则抛出异常
            if (t.isAlive())
              throw new IllegalThreadStateException();
            // 2.6 将线程加入线程队列,这里的 workers 是一个HashSet   
            workers.add(w);
            int s = workers.size();
            if (s > largestPoolSize)
              largestPoolSize = s;
            workerAdded = true;
          }
        } finally {
          mainLock.unlock();
        }
        if (workerAdded) {
          // 2.7 开启线程执行任务
          t.start();
          workerStarted = true;
        }
    }
  } finally {
    if (! workerStarted)
      addWorkerFailed(w);
  }
  return workerStarted;
}

至此,线程创建成功并开始执行任务,执行完后线程的生命周期就结束了,那么线程池如何保证 Worker 执行完任务后仍然不结束呢?当线程空闲超时或者关闭线程池又是怎样进行线程回收的呢?其中的实现逻辑就在 Worker 当中

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
  // 执行任务的线程
  final Thread thread;
  // 初始化 Worker 传进来的任务,可能为 null,如果不为空则立即执行这个 task
  Runnable firstTask;

  Worker(Runnable firstTask) {
    setState(-1);
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
  }
  
  // 线程的真正执行逻辑
  public void run() {
    runWorker(this);
  }
  
  final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    // 取出 Worker 中的任务,可能为空
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
      // task 不为 null 或者阻塞队列中有任务,通过循环不断的从阻塞队列中取出任务执行
      while (task != null || (task = getTask()) != null) {
        w.lock();
        // ...
        try {
          // 任务执行前的 hook 点
          beforeExecute(wt, task);
          try {
            // 执行任务
            task.run();
            // 任务执行后的 hook 点
            afterExecute(task, null);
          } catch (Throwable ex) {
            afterExecute(task, ex);
            throw ex;
          }
        } finally {
          task = null;
          w.completedTasks++;
          w.unlock();
        }
      }
      completedAbruptly = false;
    } finally {
      // 超时没有取到任务,则回收空闲超时的线程
      processWorkerExit(w, completedAbruptly);
    }
  }
  // ...
}

runWorker 方法的核心逻辑就是不断通过 getTask 方法从阻塞队列中获取任务并执行,因此线程的保活逻辑也在该方法中

private Runnable getTask() {

  boolean timedOut = false;

  for (;;) {
    int c = ctl.get();
    // ...

    // 如果配置了 allowCoreThreadTimeOut == true 
    // 或者线程池中的线程数大于核心线程数,则 timed = true,表示开启指定线程超时后被回收
    boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    
    // ...

    try {
      // 取出阻塞队列中的任务,如果 timed = true,则会调用阻塞队列的 poll 方法,
      // 并设置超时时间为 keepAliveTime,如果超时没有取到任务则会返回 null
      Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
      if (r != null)
        return r;
      timedOut = true;
    } catch (InterruptedException retry) {
      timedOut = false;
    }
  }
}

timed 为 true 的情况下调用阻塞队列的 poll 方法,并传入超时时间为 keepAliveTime,poll 方法是一个阻塞方法,在没有任务时会阻塞,如果在 keepAliveTime 时间内没有获取到任务就会返回 null,runWorker 方法的循环结束,线程也就结束被回收了

timed 为 false 的情况下调用阻塞队列的 take 方法,take 方法也是一个阻塞方法,在没有任务时会一直阻塞等待,这个线程也就一直保活了

综合以上,我们可以得出结论:并不是线程池的所有线程都需要一直保活,只有核心线程需要保活,非核心线程就不需要保活,超过设置的空闲时间就会被回收

posted @ 2021-03-31 22:28  低吟不作语  阅读(1155)  评论(1编辑  收藏  举报