Java线程池ThreadPoolExecutor

线程池深入解析
本文详细解析了线程池的工作原理,包括如何正确配置参数避免OOM及线程耗尽问题,探讨了线程池的使用场景及高效处理任务的方法。

线程池的使用

使用线程池管理线程可以最大程度的利用线程,节省资源消耗,它通过利用已有的线程多次循环执行多个任务从而提高系统的处理能力。我们可以通过java.util.concurrent.ThreadPoolExecutor类来创建线程池。

但是除了使用ThreadPoolExecutor,还可以使用Executors中相应的便捷方法来创建线程池,比如Executors.newFixedThreadPool(int nThreads),但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。
Executors创建线程池便捷方法列表:

方法名功能
newFixedThreadPool(int nThreads)创建固定大小的线程池
newSingleThreadExecutor()创建只有一个线程的线程池
newCachedThreadPool()创建一个不限线程数上限的线程池,任何提交的任务都将立即执行

小程序使用这些快捷方法没什么问题,对于服务端需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor的构造方法。没错,上述Executors方法创建的线程池就是使用ThreadPoolExecutor。 

 

ThreadPoolExecutor构造方法 :

Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
  int maximumPoolSize, // 线程数的上限
  long keepAliveTime, // 超过corePoolSize的线程的idle时长,超过这个时间,多余的线程会被回收。
  TimeUnit unit,  // 存活时长keepAliveTime的时间单位
  BlockingQueue<Runnable> workQueue, // 任务的排队队列
  ThreadFactory threadFactory, // 新线程的产生方式
  RejectedExecutionHandler handler // 拒绝策略) 

这些参数中需要注意一下几个参数:

  1. corePoolSize和maximumPoolSize设置不当会影响效率,甚至耗尽线程;

  2. workQueue设置不当容易导致OOM;

  3. handler设置不当会导致提交任务时抛出异常。

  4. TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),秒(SECONDS),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

下面用一个装修公司的例子来更深的理解这些参数:

装修公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。

线程池就是程序中的“装修公司”,代劳各种脏活累活。上面的过程对应到线程池上:

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 正式工数量
  int maximumPoolSize, // 工人数量上限,包括正式工和临时工
  long keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇
  BlockingQueue<Runnable> workQueue, // 排期队列
  ThreadFactory threadFactory, // 招人渠道
  RejectedExecutionHandler handler) // 拒单方式

 

线程池的工作顺序 : 

如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略。

当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

 

Runnable和Callable的区别:

可以向线程池提交的任务有两种:Runnable和Callable,

方法签名不同,void Runnable.run(), V Callable.call() throws Exception

是否允许有返回值,Callable允许有返回值

是否允许抛出异常,Callable允许抛出异常。

Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。

 

三种提交任务的方式:

提交方式是否关心返回结果
Future<T> submit(Callable<T> task)
void execute(Runnable command)
Future<?> submit(Runnable task)否,虽然返回Future,但是其get()方法总是返回null

 

正确的使用线程池

 避免使用无界队列

不要使用Executors.newXXXThreadPool()快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
                0, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
                new ThreadPoolExecutor.DiscardPolicy());

明确拒绝任务时的行为 

任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,接口定义如下:

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

 常见的拒绝策略:

       

拒绝策略拒绝行为
AbortPolicy抛出RejectedExecutionException
DiscardPolicy什么也不做,直接忽略
DiscardOldestPolicy丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置
CallerRunsPolicy直接由提交任务者执行这个任务

线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。 

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
                0, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<>(512), 
                new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略

 

获取处理结果和异常 

线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionException,submit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写: 

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
                0, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<>(512), 
                new ThreadPoolExecutor.DiscardPolicy());

Future<Object> future = executorService.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者
        }
    });
    
try {
  Object result = future.get();
} catch (InterruptedException e) {
  // interrupt
} catch (ExecutionException e) {
  // exception in Callable.call()
  e.printStackTrace();
}

 

线程池的常用场景 

获取单个结果

通过submit()向线程池提交任务后会返回一个Future,调用V Future.get()方法能够阻塞等待执行结果,V get(long timeout, TimeUnit unit)方法可以指定等待的超时时间。

获取多个结果

如果向线程池提交了多个任务,要获取这些任务的执行结果,可以依次调用Future.get()获得。但对于这种场景,我们更应该使用ExecutorCompletionService,该类的take()方法总是阻塞等待某一个任务完成,然后返回该任务的Future对象。向CompletionService批量提交任务后,只需调用相同次数的CompletionService.take()方法,就能获取所有任务的执行结果,获取顺序是任意的,取决于任务的完成顺序:

void solve(Executor executor, Collection<Callable<Result>> solvers)
   throws InterruptedException, ExecutionException {
   
   CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 构造器
   
   for (Callable<Result> s : solvers)// 提交所有任务
       ecs.submit(s);
       
   int n = solvers.size();
   for (int i = 0; i < n; ++i) {// 获取每一个完成的任务
       Result r = ecs.take().get();
       if (r != null)
           use(r);
   }
}

单个任务的超时时间

V Future.get(long timeout, TimeUnit unit)方法可以指定等待的超时时间,超时未完成会抛出TimeoutException。

多个任务的超时时间

等待多个任务完成,并设置最大等待时间,可以通过CountDownLatch完成:

public void testLatch(ExecutorService executorService, List<Runnable> tasks) 
    throws InterruptedException{
      
    CountDownLatch latch = new CountDownLatch(tasks.size());
      for(Runnable r : tasks){
          executorService.submit(new Runnable() {
              @Override
              public void run() {
                  try{
                      r.run();
                  }finally {
                      latch.countDown();// countDown
                  }
              }
          });
      }
      latch.await(10, TimeUnit.SECONDS); // 指定超时时间
  }

 

总结

Executors为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch完成。

<think>我们刚刚讨论过Java线程池ExecutorService的全面内容,现在用户明确要求了解ThreadPoolExecutor的具体信息。从用户的问题和引用来看,需要聚焦ThreadPoolExecutor的使用方法、工作原理和应用场景。 用户提供的引用材料强调ThreadPoolExecutorJava线程池的核心实现,这点非常重要。引用[1]提到它管理预先创建的线程提高性能,引用[2]强调其配置选项和设计思想,引用[3]说明线程复用机制,引用[4]则指出其类继承关系。这些都需要整合到回答中。 考虑到用户可能是开发人员,我将从三个维度构建回答:使用方式会重点演示创建参数和任务提交,原理部分解析任务调度和线程复用机制,应用场景则结合不同业务需求。特别要注意引用[3]提到的任务队列选择和线程复用特性,这关系到实际性能表现。 在回答结构上,准备用三大部分展开:使用指南包含创建线程池的代码示例和工作流程说明;核心原理部分会解释状态管理和任务调度流程;应用场景则对比不同线程池类型。每个关键点都会自然标注引用来源,比如线程复用机制说明后添加[^3]。 最后会生成三个相关问题:钩子方法的使用、拒绝策略的深度解析、以及生产环境监控方案。这些问题既延续当前主题,又能帮助用户进阶学习。 </think>以下是关于 Java 线程池 `ThreadPoolExecutor` 的核心解析,涵盖使用、原理及应用场景: --- ### 一、使用指南 #### 1. 创建线程池(推荐手动配置) ```java ThreadPoolExecutor executor = new ThreadPoolExecutor( 4, // 核心线程数(常驻) 10, // 最大线程数 60, TimeUnit.SECONDS, // 非核心线程空闲存活时间 new ArrayBlockingQueue<>(100), // 有界任务队列 Executors.defaultThreadFactory(), // 线程工厂 new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 ); ``` - **关键参数**: - **核心线程**:默认常驻,即使空闲也不回收 - **任务队列**:推荐有界队列(如 `ArrayBlockingQueue`)防 OOM - **拒绝策略**:默认 `AbortPolicy`(队列满时抛异常) #### 2. 提交任务 ```java // 无返回值任务 executor.execute(() -> System.out.println("Task running")); // 有返回值任务 Future<String> future = executor.submit(() -> { Thread.sleep(1000); return "Result"; }); String result = future.get(); // 阻塞获取结果 ``` #### 3. 关闭线程池 ```java executor.shutdown(); // 优雅关闭:停止接收新任务,处理完队列任务 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); // 强制终止剩余任务 } ``` > **最佳实践**:`shutdown()` + `awaitTermination()` 组合确保安全关闭[^3] --- ### 二、核心原理 #### 1. 任务调度流程 1. 提交任务 → 优先使用核心线程执行 2. 核心线程满载 → 任务进入阻塞队列 3. 队列满载 → 创建非核心线程(不超过 `maxPoolSize`) 4. 线程和队列均满 → 触发拒绝策略[^1] #### 2. 线程复用机制 - **Worker 工作线程**: - 封装 `Thread` 和初始任务 `firstTask` - 循环执行 `getTask()` 从队列获取新任务 - **线程回收**: - 非核心线程空闲超过 `keepAliveTime` 后自动销毁 - 核心线程默认常驻(可通过 `allowCoreThreadTimeOut(true)` 配置回收) #### 3. 状态管理(原子变量 `ctl`) $$ \begin{array}{c|c} \text{高 3 位} & \text{低 29 位} \\ \hline \text{RUNNING} & \text{线程数} \\ \text{SHUTDOWN} & \\ \text{STOP} & \\ \text{TIDYING} & \\ \text{TERMINATED} & \\ \end{array} $$ > 状态转换:RUNNING → SHUTDOWN → TIDYING → TERMINATED[^4] --- ### 三、应用场景 | **场景类型** | **推荐配置** | **案例** | |---------------------|---------------------------------------|------------------------------| | **CPU 密集型** | 线程数 ≈ CPU 核数 | 数据加密、图像处理 | | **I/O 密集型** | 线程数 = CPU 核数 × (1 + 等待时间/计算时间) | 网络请求、文件读写[^2] | | **定时任务** | 使用 `ScheduledThreadPoolExecutor` | 定时报表、心跳检测 | | **高吞吐异步处理** | `CachedThreadPool` + 限流 | 消息队列消费(需防 OOM)[^3] | --- ### 四、调优与问题解决 1. **线程数公式**: $$ N_{\text{threads}} = N_{\text{CPU}} \times U_{\text{CPU}} \times (1 + \frac{W}{C}) $$ - $N_{\text{CPU}}$:CPU 核数(`Runtime.getRuntime().availableProcessors()`) - $U_{\text{CPU}}$:目标 CPU 利用率(建议 0.8) - $W/C$:等待时间与计算时间的比值 2. **常见问题**: - **任务堆积** → 使用有界队列 + `CallerRunsPolicy` 策略 - **线程泄露** → 通过 `ThreadFactory` 添加 `UncaughtExceptionHandler` - **CPU 过高** → 降低线程数或优化任务逻辑[^1] > **监控指标**: > ```java > executor.getActiveCount(); // 活动线程数 > executor.getQueue().size(); // 队列积压量 > executor.getCompletedTaskCount(); // 已完成任务 > ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值