【桦说并发下篇】漫谈线程池

231 阅读11分钟

【桦说并发下篇】漫谈线程池

禁止转载。

上篇请点击这里。

本文提及的线程池可以指代具体的线程池或者线程池提供的服务(ExecutorService)。

理想的线程池

  1. 作为任务执行的载体,两者解耦,任务无需关注自己是由哪个线程执行的。
  2. Bulkhead 模式,根据任务类型的不同(IO 密集型任务、CPU 密集型任务、延迟任务、执行多次的定时任务、限制超时时间的任务、多种任务组合的任务等),应该由不同的线程池执行。使用时可以对线程池进行封装。
  3. 作为一种有限资源,线程支持复用和回收,理想情况是恰好满足任务需要,极端情况下避免资源耗尽。
  4. 可动态配置,可监控。
  5. 任务执行支持回调,任务可编排,可取消。

Bulkhead 模式

中文名为舱壁模式,bulkhead 指的是划分船舱的舱壁,使用舱壁可以保证当一部分船舱漏水时,船只整体不至于下沉。适用于系统容错设计,不让一小部分模块或组件的失效影响整体的可用性。根据任务的不同,不同的线程池应该隔离使用,使用线程池是实现 Bulkhead 模式的常用方案。Resilience4J 提供了相关支持。

线程池作为上下文

// 任务执行和线程池解耦的伪代码表示
CompletableFuture<List<User>> users;
// IO密集型任务
IO_CONTEXT {
  var u1 = getUser("a");
  var u2 = getUser("b");
  var u3 = getUser("c");
  // CPU 密集型任务
  CPU_CONTEXT {
    users = report(u1, u2, u3);
  }
}
return users.join();

实现中的问题

1. 上下文传递

虽然基于 ThreadLocal 的上下文传递并不是编码的最佳实践,但是现状是很多框架、监控系统使用了 ThreadLocal 传递上下文,可以实现无侵入性的代码功能增强。Java 官方并没有提供线程池 ThreadLocal 方案。TransmittableThreadLocal(TTL)支持了线程池传递上下文的问题。

2. ExecutorService 和 ScheduledExecutorService

标准库提供的接口,可拓展。

提供了异步执行方法,submit 返回 Future。但是 submitAny 和 submitAll 均为阻塞方法。

ScheduledExecutorService 提供定时任务执行方法:schedule(任务执行一次),scheduleAtFixedRate(任务执行多次),scheduleWithFixedDelay(任务执行多次),对返回结果 Future 使用 cancel 可以取消任务。

最大的问题是返回的 Future 对象不支持回调,直接使用只能调用阻塞方法 get。

3. ThreadPoolExecutor 和 ScheduledThreadPoolExecutor

可以满足绝大多数任务的需要,支持配置不同的线程池参数:核心线程、临时线程、任务队列、线程工厂、拒绝策略等。

支持回调函数:beforeExecute, afterExecute, terminate。

具体执行流程、状态变化、优雅关闭等内容不再赘述,这里仅说明其常用配置方法:

  • 阻塞队列的选择:阻塞队列按照长度划分为 SynchronousQueue(0)、ArrayBlockingQueue(有限值)、LinkedBlockingDeque(有限值或无限)。SynchronousQueue 适用于数据直接传递,其他的 blockingQueue 保证任务进入队列,待到特定条件执行。DelayedWorkQueue 底层使用二叉堆,适用于优先级(延迟时间)排序。
  • IO 密集型任务:
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

以上工具类方法适用于短期大量请求,但是可能造成大量线程同时段创建与销毁,解决方案是

1. 指定最大线程数(足够大,同时系统又不至于崩溃)和拒绝策略,设置好兜底策略。可以自行封装成 SafeExecutors 工具类或者使用相关类库如 Resilience4J Bulkhead。
1. 限流
1. 流量监控+告警

总之,IO 密集型任务不会占用多少 CPU,不必追求 CPU 占用率而提高线程数。临时线程数应该满足短期内任务的需要。

  • CPU 密集型任务
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

以上工具类方法可以接受无限的任务,可能会造成任务延后执行并失效,应该设置为满足业务相关的队列大小。解决方法为:

1. 指定队列大小和拒绝策略,设置好兜底策略。可以自行封装成 SafeExecutors 工具类或者使用相关类库如 Resilience4J Bulkhead。
1. 限流
1. 流量监控+告警
  • 混合型任务

如拉取消息队列数据后,进行聚合处理。一般可以处理为多个线程池,各司其职。

  • 单线程线程池

和直接 new Thread() 结果一致,但是可以方便监控,避免错误使用,多个任务依次执行,一定程度上避免并发问题。

任务包装逻辑

这里不讨论具体的线程执行任务流程,仅讨论 AbstractExecutorService 对任务的封装。

public abstract class AbstractExecutorService implements ExecutorService {
  protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
  }
  protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new FutureTask<T>(callable);
  }
  public <T> Future<T> submit(Runnable task, T result) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task, result);
    execute(ftask);
    return ftask;
  }
  // 略去其他
}

从代码中可得知,submit 内部调用了 execute 方法,后续子类只需要实现 execute 方法即可。FutureTask 实际上是 ThreadPoolExectutor 返回 Future 的具体实现。FutureTask 实现了 Runable、Future 接口,同时支持结果的同步。

4. ForkJoinPool

本意是基于工作窃取算法,支持可递归任务,提高并发效率,但是被严重滥用。

ForkJoinPool 类提供了 commonPool 方法,可以获取全局公用线程池,无法手动关闭。Stream 流式编程和 CompletableFuture 默认使用了全局公用的线程池,违反了 Bulkhead 模式,不建议使用。尽管 ForkJoinPool 尽力保证不会出现不活跃线程,如果部分任务执行出现错误,仍然会影响其他任务的执行。而且出现错误的概率比单线程编程高很多,可能出现的问题不限于:提交了阻塞 IO 任务、死锁、饥饿、线程不安全。对于多种任务的执行情况,无法监控。

总之,一般业务开发不建议使用。

Spring 拓展

// 官方示例  
	@Async
  public CompletableFuture<User> findUser(String user) throws InterruptedException {
    logger.info("Looking up " + user);
    String url = String.format("https://api.github.com/users/%s", user);
    User results = restTemplate.getForObject(url, User.class);
    // Artificial delay of 1s for demonstration purposes
    Thread.sleep(1000L);
    return CompletableFuture.completedFuture(results);
  }

使用@Async 注解和配置的线程池(一般是 ThreadPoolTaskExecutor),findUser 方法被代理后,会自动异步执行,无需手动 submit。实现了线程池和任务的解耦。

ThreadPoolTaskExecutor 支持回调:

@Deprecated(since = "6.0")
public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor {
  // Spring 借鉴 Guava 自己实现的 ListenableFuture
	ListenableFuture<?> submitListenable(Runnable task);
}

// 6.0 支持返回 CF
public interface AsyncTaskExecutor extends TaskExecutor {
  default CompletableFuture<Void> submitCompletable(Runnable task) {
    return CompletableFuture.runAsync(task, this);
  }
}

TheadPoolExecutor 的回调方法改为可以通过参数配置:

public void setTaskDecorator(TaskDecorator taskDecorator) {
    this.taskDecorator = taskDecorator;
  	// 使用组合实现继承功能:更好用和容易理解
}

可动态配置("corePoolSize", "maxPoolSize", "keepAliveSeconds"),可监控,提供如 getQueueSize, getActiveCount, getKeepAliveSeconds 等方法。

Spring 支持对线程池进行生命周期管理,通过自定义配置初始化,生命周期结束时销毁 bean:

// 父类实现生命周期相关方法
public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
  implements BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean,
SmartLifecycle, ApplicationListener<ContextClosedEvent> {
  @Override
  public void afterPropertiesSet() {
    // 初始化回调:InitializingBean#afterPropertiesSet
    // 实际上这个方法可以 inline
    initialize();
  }

  // 初始化模版,子类实现 initializeExecutor
  public void initialize() {
    if (logger.isDebugEnabled()) {
      logger.debug("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
    }
    if (!this.threadNamePrefixSet && this.beanName != null) {
      setThreadNamePrefix(this.beanName + "-");
    }
    this.executor = initializeExecutor(this.threadFactory, this.rejectedExecutionHandler);
    this.lifecycleDelegate = new ExecutorLifecycleDelegate(this.executor);
  }

  protected abstract ExecutorService initializeExecutor(
    ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler);
}

public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
  implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {
  @Override
  protected ExecutorService initializeExecutor(
    ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
		// 初始化核心逻辑
    BlockingQueue<Runnable> queue = createQueue(this.queueCapacity);
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
      this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
      queue, threadFactory, rejectedExecutionHandler) {
      @Override
      public void execute(Runnable command) {
        // 任务增强, 其他 submit 方法会复用 execute 方法,详见 AbstractExecutorService
        Runnable decorated = command;
        if (taskDecorator != null) {
          decorated = taskDecorator.decorate(command);
          if (decorated != command) {
            decoratedTaskMap.put(decorated, command);
          }
        }
        super.execute(decorated);
      }
      @Override
      protected void beforeExecute(Thread thread, Runnable task) {
        ThreadPoolTaskExecutor.this.beforeExecute(thread, task);
      }
      @Override
      protected void afterExecute(Runnable task, Throwable ex) {
        ThreadPoolTaskExecutor.this.afterExecute(task, ex);
      }
    };
    // 其他配置信息
    if (this.allowCoreThreadTimeOut) {
      executor.allowCoreThreadTimeOut(true);
    }
    if (this.prestartAllCoreThreads) {
      executor.prestartAllCoreThreads();
    }
    this.threadPoolExecutor = executor;
    return executor;
  }
}

再来看看优雅关闭的实现:

public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
  implements BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean,
SmartLifecycle, ApplicationListener<ContextClosedEvent> {
  @Override
  public void destroy() {
    // 实际上这个方法可以 inline
    shutdown();
  }

  public void shutdown() {
    if (logger.isDebugEnabled()) {
      logger.debug("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
    }
    if (this.executor != null) {
      // 判断是否等待任务执行完
      if (this.waitForTasksToCompleteOnShutdown) {
        // 拒绝新任务
        this.executor.shutdown();
      }
      else {
        for (Runnable remainingTask : this.executor.shutdownNow()) {
          // 取消任务
          cancelRemainingTask(remainingTask);
        }
      }
      // 等待完全关闭
      awaitTerminationIfNecessary(this.executor);
    }
  }
	// 取消任务逻辑,实际上任务已经包装为 FutureTask, 详见 AbstractExecutorService
  protected void cancelRemainingTask(Runnable task) {
    if (task instanceof Future<?> future) {
      future.cancel(true);
    }
  }

  private void awaitTerminationIfNecessary(ExecutorService executor) {
    if (this.awaitTerminationMillis > 0) {
      try {
        // 等待完全关闭
        if (!executor.awaitTermination(this.awaitTerminationMillis, TimeUnit.MILLISECONDS)) {
          if (logger.isWarnEnabled()) {
            logger.warn("Timed out while waiting for executor" +
                        (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
          }
        }
      }
      // 标准的中断处理方式: 日志+再打断
      catch (InterruptedException ex) {
        if (logger.isWarnEnabled()) {
          logger.warn("Interrupted while waiting for executor" +
                      (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
        }
        Thread.currentThread().interrupt();
      }
    }
  }
}

public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
  implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {
  // 取消剩余任务支持传播,包装任务 -> 取消原任务
  @Override
  protected void cancelRemainingTask(Runnable task) {
    super.cancelRemainingTask(task);
    // Cancel associated user-level Future handle as well
    Object original = this.decoratedTaskMap.get(task);
    if (original instanceof Future<?> future) {
      future.cancel(true);
    }
  }
}

Guava:: ListeningExecutorService

// 需要使用 ListenableFuture 时,应该优先使用此接口
public interface ListeningExecutorService extends ExecutorService {
  // 返回 Future 子类: ListenableFuture, 继承时,子类/子接口支持协变
  <T extends @Nullable Object> ListenableFuture<T> submit(Callable<T> task);
  ListenableFuture<?> submit(Runnable task);
  <T extends @Nullable Object> ListenableFuture<T> submit(
    Runnable task, @ParametricNullness T result);
  // 略去 invokeAll, invokeAny 不推荐使用,均为阻塞方法
}

回调实现

// -33.2.1-jre
public abstract class AbstractListeningExecutorService extends AbstractExecutorService
  implements ListeningExecutorService {

  @Override
  protected final <T extends @Nullable Object> RunnableFuture<T> newTaskFor(
    Runnable runnable, @ParametricNullness T value) {
    return TrustedListenableFutureTask.create(runnable, value);
  }
}

// 为了完整性,以下代码展示了此 FutureTask 实现回调的具体过程
// 继承链比较长
class TrustedListenableFutureTask<V extends @Nullable Object> extends FluentFuture.TrustedFuture<V> implements RunnableFuture<V> {}

abstract static class TrustedFuture<V extends @Nullable Object> extends FluentFuture<V>
  implements AbstractFuture.Trusted<V> {}

public abstract class FluentFuture<V extends @Nullable Object>
  extends GwtFluentFutureCatchingSpecialization<V> {}

abstract class GwtFluentFutureCatchingSpecialization<V extends @Nullable Object>
  extends AbstractFuture<V> {}

public abstract class AbstractFuture<V extends @Nullable Object> extends InternalFutureFailureAccess implements ListenableFuture<V> {}

// 具体实现在 AbstractFuture 中
public abstract class AbstractFuture<V extends @Nullable Object> extends InternalFutureFailureAccess implements ListenableFuture<V> {
  @CheckForNull private volatile Listener listeners;
  // 略去其他
  public void addListener(Runnable listener, Executor executor) {
    checkNotNull(listener, "Runnable was null.");
    checkNotNull(executor, "Executor was null.");
    if (!isDone()) {
      // 新增 listener, 使用线程安全链表,《并发编程实战》有相似的例子
      Listener oldHead = listeners; // 读操作
      if (oldHead != Listener.TOMBSTONE) {
        Listener newNode = new Listener(listener, executor);
        do {
          newNode.next = oldHead;
          // 静态条件下,每次仅有一个新头部可以设置成功
          if (ATOMIC_HELPER.casListeners(this, oldHead, newNode)) {
            return;
          }
          oldHead = listeners; // re-read
        } while (oldHead != Listener.TOMBSTONE);
      }
    }
    // If we get here then the Listener TOMBSTONE was set, which means the future is done, call
    // the listener.
    // future 已完成 -> 直接执行
    executeListener(listener, executor);
  }
}

// 回调调用 complete 方法,在 ListenableFuture 设置值后调用
// 如 set(V), setException(Throwable), setFuture(ListenableFuture)方法
private static void complete(AbstractFuture<?> param, boolean callInterruptTask) {
    // Declare a "true" local variable so that the Checker Framework will infer nullness.
    AbstractFuture<?> future = param;
    Listener next = null;
    outer:
    while (true) {
      future.releaseWaiters();
      if (callInterruptTask) {
        future.interruptTask();
        callInterruptTask = false;
      }
      future.afterDone();
      // push the current set of listeners onto next
      next = future.clearListeners(next);
      future = null;
      while (next != null) {
        // 调用所有回调 listeners
        Listener curr = next;
        next = next.next;
        // curr 记录了之前传参: task(Runable) + executor
        Runnable task = requireNonNull(curr.task);
        if (task instanceof SetFuture) {
          SetFuture<?> setFuture = (SetFuture<?>) task;
          future = setFuture.owner;
          if (future.value == setFuture) {
            Object valueToSet = getFutureValue(setFuture.future);
            if (ATOMIC_HELPER.casValue(future, setFuture, valueToSet)) {
              continue outer;
            }
          }
        } else {
          executeListener(task, requireNonNull(curr.executor));
        }
      }
      break;
    }
  }

使用建议

使用官方推荐方式(装饰器模式)创建线程池,MoreExecutors.listeningDecorator(ExecutorService delegate) -> ListeningExecutorService

netty 中的线程池与异步实现

EventExecutorGroup 继承自 ScheduledExecutorService,提交任务返回 netty 的 Future 实现(io.netty.util.concurrent.Future),Future 支持回调,支持优雅关闭和关闭回调。

EventExecutorGroup 用于通用的任务执行和管理,可以处理各种类型的任务,包括但不限于 IO 操作。 其实现 EventLoopGroup 专注于 IO 事件的处理和管理,支持注册 channel,通常用于处理网络连接、读取和写入操作。

提供了 Promise 接口,支持对 Future 的写操作。

总的来说,Netty 不愧为优秀的异步网络框架。

以下代码摘自 官方文档,事件/任务处理有三个线程池,bossGroup, workGroup 和 10 个线程的任务处理线程。

public void run() throws Exception {
    // 创建两个 EventLoopGroup,bossGroup 用于接受连接,workerGroup 用于处理连接
    EventLoopGroup bossGroup = new NioEventLoopGroup(); // 单线程,负责处理接受连接事件
    EventLoopGroup workerGroup = new NioEventLoopGroup(); // 多线程,负责处理已连接的 IO 事件

    try {
        // 创建 ServerBootstrap 实例,用于启动服务器
        ServerBootstrap b = new ServerBootstrap();
        // 设置 bossGroup 和 workerGroup 到 ServerBootstrap
        b.group(bossGroup, workerGroup)
         // 指定使用 NIO 传输 Channel
         .channel(NioServerSocketChannel.class)
         // 设置处理新连接数据的处理器,并配置每个 Channel 的处理器
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             public void initChannel(SocketChannel ch) throws Exception {
                 // 添加处理器到 ChannelPipeline 中,这里使用了 DefaultEventExecutorGroup 来处理 channelRead 事件
                 ch.pipeline().addLast(new DefaultEventExecutorGroup(10), new ChannelInboundHandlerAdapter() {
                     @Override
                     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                         // channelRead 事件的处理逻辑,可以提交给 executorGroup 处理
                     }
                 });
             }
         })
         // 设置参数
         .option(ChannelOption.SO_BACKLOG, 128)          // 等待连接的队列大小
         .childOption(ChannelOption.SO_KEEPALIVE, true); // 保持连接活跃
        // 绑定端口,开始接受进来的连接
        ChannelFuture f = b.bind(port).sync();
        // 等待服务器 socket 关闭
        f.channel().closeFuture().sync();
    } finally {
        // 优雅地关闭线程池和释放所有的资源
        workerGroup.shutdownGracefully();
        bossGroup.shutdownGracefully();
    }
}

动态线程池框架

DynamicTP 是基于配置中心的轻量级动态可监控线程池,提供了动态调参、通知报警、运行监控、三方包集成等功能。是对 JDK 提供的线程池的良好补充。限于篇幅所限,使用请参考 官方文档, 美团实践请参考 这篇文章

总结

  1. 线程池的配置就像虚拟机的调优,似乎是一门艺术。
  2. 推荐使用线程池+异步支持类(如 CompletableFuture)实现并发需求,尽量不要使用底层代码。
  3. 对于 CPU 密集型任务,并发执行相比单线程执行不见得有性能提升,根据具体情形,需要对任务执行进行基准测试。
  4. 尽管标准库不尽如人意,实际项目中可以选择不同的增强类库,但是最好保持统一,避免混用。