【Java EE】线程池

线程池

为什么要使用线程池?

在Java EE应用中,如果每次需要执行任务时都创建一个新线程,会带来以下问题:

  • 资源消耗大:线程的创建和销毁都需要消耗系统资源
  • 稳定性差:无限制创建线程会导致系统资源耗尽
  • 管理困难:无法统一管理线程的状态和生命周期

线程池通过复用线程解决了这些问题,能够有效控制并发线程数量,提高系统响应速度。

线程池的核心实现:ThreadPoolExecutor

Java提供了java.util.concurrent.ThreadPoolExecutor作为线程池的标准实现。

核心构造参数⭐

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,      // 核心线程数
    int maximumPoolSize,   // 最大线程数
    long keepAliveTime,    // 空闲线程存活时间
    TimeUnit unit,         // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
)

核心线程数:corePoolSize

线程池初始化时会创建的核心线程数量 。这些线程会一直存活(除非显式设置 allowCoreThreadTimeOut=true,允许核心线程空闲时被销毁)。当有任务提交时,优先由核心线程执行;核心线程全忙时,任务才会进入工作队列。

最大线程数:maximumPoolSize

线程池能容纳的最大线程数 (核心线程 + 非核心线程)。当工作队列满了,且当前线程数小于最大线程数时,线程池会创建非核心线程 来处理任务;非核心线程在空闲时会被销毁(由 keepAliveTime 控制),实现线程数量的自适应调整。

非核心线程空闲存活时间:keepAliveTime

非核心线程在空闲状态 下允许存活的最长时间。超过该时间,非核心线程会被销毁,回收资源(核心线程默认不因空闲被销毁,除非开启 allowCoreThreadTimeOut)。

统一表示时间的单位: unit

TimeUnit unit 通过枚举定义时间单位 ,让 keepAliveTime(或其他时间参数)的语义更明确,避免因时间单位歧义导致的逻辑错误(比如误将"毫秒"当"秒"用,导致线程存活时间过短/过长)。

  • NANOSECONDS:纳秒(1秒 = 10⁹ 纳秒)
  • MICROSECONDS:微秒(1秒 = 10⁶ 微秒)
  • MILLISECONDS:毫秒(1秒 = 10³ 毫秒)
  • SECONDS:秒(最常用)
  • MINUTES:分钟(1分钟 = 60秒)
  • HOURS:小时(1小时 = 60分钟)
  • DAYS:天(1天 = 24小时)

工作队列:workQueue

用于存放待执行任务的阻塞队列 。当核心线程都在处理任务时,新提交的任务会进入队列等待;队列满了,才会触发非核心线程的创建。线程池本质是生产者-消费者模型submit/execute 任务是"生产者",线程是"消费者",队列是缓冲区)。

线程工厂:threadFactory

作用 :用于创建线程的工厂类,可自定义线程属性(如线程名称、优先级、是否为守护线程等),方便调试和监控线程池中的线程(如给线程命名"pool-1-thread-1"便于定位问题)。

工厂模式

拒绝策略: handler⭐

作用 :当线程池和队列都满了(即达到 maximumPoolSize 且队列满),新提交的任务会被拒绝handler 定义了拒绝时的处理逻辑(如抛异常、由调用者执行任务、丢弃任务等)。

常见策略

  • AbortPolicy(默认):抛 RejectedExecutionException 异常。
  • CallerRunsPolicy:由提交任务的线程(调用者)执行任务。
  • DiscardPolicy:直接丢弃任务。
  • DiscardOldestPolicy:丢弃队列中最老的任务,尝试加入新任务。

任务执行流程⭐

当向线程池提交一个任务时,执行顺序如下:
可视化入口

常用的线程池类型

Java标准库通过 Executors 工具类,基于工厂设计模式ThreadPoolExecutor 进行了封装,简化了线程池的创建和使用。

Executor 框架继承层级

Executor 框架继承层级

Executors 提供的工厂方法

工厂方法 返回类型 特点 隐藏风险
newFixedThreadPool(int n) ThreadPoolExecutor 固定线程数,使用无界队列 队列无界,任务积压可能导致 OOM
newCachedThreadPool() ThreadPoolExecutor 线程数动态增长,空闲线程存活60秒 最大线程数无限制,高并发时创建大量线程可能导致 OOM 或系统崩溃
newSingleThreadExecutor() ThreadPoolExecutor 单线程串行执行 同样使用无界队列,任务积压风险
newScheduledThreadPool(int n) ScheduledThreadPoolExecutor 支持定时/延迟任务 同样存在无界队列风险

FixedThreadPool⭐

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(5);
  • 核心线程数 = 最大线程数,线程数量固定
  • 使用无界队列LinkedBlockingQueue
  • 适用:负载较重的服务器

CachedThreadPool⭐

java 复制代码
ExecutorService pool = Executors.newCachedThreadPool();
  • 核心线程数为0,最大线程数为Integer.MAX_VALUE
  • 空闲线程存活60秒
  • 使用同步队列SynchronousQueue
  • 适用:大量短生命周期的任务

SingleThreadExecutor

java 复制代码
ExecutorService pool = Executors.newSingleThreadExecutor();
  • 只有一个核心线程,所有任务串行执行
  • 适用:保证任务按顺序执行的场景

ScheduledThreadPool

java 复制代码
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
  • 支持定时和周期性任务执行
  • 适用:延迟任务、定时任务

submit() 方法

submit()ExecutorService 接口中定义的核心方法,用于向线程池提交任务并返回一个 Future 对象,便于获取任务执行结果或跟踪任务状态。

java 复制代码
// 提交 Runnable 任务,返回 Future<?>,get() 返回 null
Future<?> submit(Runnable task);

// 提交 Runnable 任务,并指定返回结果
<T> Future<T> submit(Runnable task, T result);

// 提交 Callable 任务,返回 Future<T>,get() 返回计算结果
<T> Future<T> submit(Callable<T> task);

submit(Runnable task)

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(2);

Future<?> future = pool.submit(() -> {
    System.out.println("执行任务...");
    // 模拟耗时操作
    Thread.sleep(1000);
});

// get() 返回 null,因为 Runnable 没有返回值
Object result = future.get();  // result = null
System.out.println("任务完成,结果:" + result);

特点

  • 返回 Future<?>,泛型类型是 Void
  • future.get() 返回 null
  • 主要用途:等待任务执行完成,而非获取结果

submit(Runnable task, T result)

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(2);

// 定义一个结果对象
String resultObj = "初始值";

// 提交任务,并指定返回的结果
Future<String> future = pool.submit(() -> {
    System.out.println("执行任务...");
    // 注意:Runnable 内部无法修改 resultObj 的引用指向,
    // 但可以修改 resultObj 对象的内部状态
}, resultObj);

// get() 返回的是传入的 resultObj 对象
String result = future.get();
System.out.println("结果:" + result);  // 结果:初始值

典型使用场景 :需要知道哪个任务完成了,配合一个可变的容器对象使用:

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(3);
List<Future<AtomicInteger>> futures = new ArrayList<>();

for (int i = 0; i < 10; i++) {
    AtomicInteger counter = new AtomicInteger(i);
    Future<AtomicInteger> future = pool.submit(() -> {
        // 模拟业务处理
        Thread.sleep(100);
        // 注意:这里可以修改 counter 的内部状态
        counter.set(counter.get() * 2);
    }, counter);
    futures.add(future);
}

for (Future<AtomicInteger> future : futures) {
    AtomicInteger result = future.get();
    System.out.println("处理结果:" + result.get());
}

submit(Callable task) ⭐ 最常用

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(2);

Future<Integer> future = pool.submit(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        // 执行计算,可以返回结果
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
});

// Lambda 写法
Future<Integer> future2 = pool.submit(() -> {
    int sum = 0;
    for (int i = 1; i <= 100; i++) {
        sum += i;
    }
    return sum;
});

Integer result = future.get();  // 阻塞直到获取结果
System.out.println("1+2+...+100 = " + result);  // 5050

特点

  • Callable 有返回值,且可以抛出受检异常
  • Future.get() 返回计算结果
  • 企业开发中最常用的方式

三种方式的对比

方法签名 返回值获取 异常处理 适用场景
submit(Runnable) get() 返回 null 通过 Future.get() 捕获 只需要等待完成,不需要结果
submit(Runnable, T) get() 返回传入的对象 通过 Future.get() 捕获 需要标识完成的任务
submit(Callable<T>) get() 返回计算结果 通过 Future.get() 捕获 需要异步计算结果的场景

submit() 让代码有了跟踪和控制异步任务的能力,这是 execute() 无法做到的。在实际企业开发中,优先使用 submit(Callable) 来处理异步任务。

submit() vs execute()

特性 execute(Runnable) submit(Runnable/Callable)
返回值 void Future<T>
获取执行结果 ❌ 不支持 ✅ 支持
捕获异常 需在任务内捕获 ✅ 可通过 Future.get() 捕获
取消任务 ❌ 不支持 ✅ 支持
判断任务完成状态 ❌ 不支持 ✅ 支持

submit() 执行流程

text 复制代码
提交任务
    ↓
┌─────────────────────────────────────────┐
│  submit(Runnable) / submit(Callable)    │
└─────────────────────────────────────────┘
    ↓
将 Runnable/Callable 包装成 RunnableFuture
    ↓
添加到工作队列,等待线程执行
    ↓
线程执行任务,捕获异常或计算结果
    ↓
任务完成,结果/异常存储在 Future 中
    ↓
调用 future.get() 获取结果(阻塞)

为什么阿里规范不建议使用 Executors 创建线程池?⭐

《阿里巴巴Java开发手册》中明确提到:

【强制】 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

原因一目了然:

  • Executors.newFixedThreadPoolnewSingleThreadExecutor 使用的队列是 LinkedBlockingQueue,默认容量为 Integer.MAX_VALUE,高并发下任务积压会导致 内存溢出
  • Executors.newCachedThreadPoolnewScheduledThreadPool 最大线程数为 Integer.MAX_VALUE,可能创建大量线程导致 CPU 和内存耗尽
方式 优点 缺点
Executors 工厂类 代码简洁,使用方便 存在 OOM 风险,参数不透明
手动 new ThreadPoolExecutor 参数可控,安全性高 代码稍显冗长

生产环境一定要手动创建 ThreadPoolExecutor,显式指定队列大小和拒绝策略。开发测试或简单场景可以酌情使用 Executors

手撕线程池

MyThreadPool核心设计思想

这个线程池主要由两部分组成:

  • 任务队列(BlockingQueue):负责存放待执行的任务。
  • 工作线程 :负责从队列中取出任务并执行。
    MyThreadPool 是消费者,main 方法(提交任务的代码)是生产者。

MyThreadPool的导入与属性

java 复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class MyThreadPool {
    private BlockingQueue<Runnable> queue = null;
  • 为什么用 BlockingQueue 它是线程安全的。普通集合(如 ArrayList)在多线程下需要手动加锁,而 BlockingQueue 内部已经处理好了并发问题。
  • 为什么用 ArrayBlockingQueue 这是一个有界队列(指定容量为1000)。使用有界队列可以防止任务无限堆积导致内存溢出(OOM)。用别的也是OK的。

阻塞队列(BlockingQueue)

构造方法:创建工作线程

java 复制代码
public MyThreadPool(int n) {
    queue = new ArrayBlockingQueue<>(1000);
    for (int i = 0; i < n; i++) {
        Thread t = new Thread(() -> {
            try {
                while (true) {
                    Runnable task = queue.take(); // 核心点1
                    task.run();                   // 核心点2
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
    }
}
  • 核心点1 queue.take() :如果队列里没有任务,take() 方法会让当前线程阻塞等待,不会占用 CPU 资源。一旦有任务放入队列,线程会被自动唤醒。
  • 核心点2 task.run() :注意这里是直接调用 run() 方法,而不是 start()。这意味着任务是在当前的工作线程中同步执行的,并没有创建新的线程。
  • while (true):让工作线程一直存活,不断拉取任务,实现线程复用。

提交任务方法:submit()

java 复制代码
public void submit(Runnable task) throws InterruptedException {
    queue.put(task);
}
  • queue.put(task) :将任务放入队列。如果队列已满(达到了1000),put() 方法也会阻塞,直到队列有空闲位置。

测试

java 复制代码
public static void main(String[] args) throws InterruptedException {
    MyThreadPool pool = new MyThreadPool(10); // 创建10个工作线程
    for (int i = 0; i < 100; i++) {
        int id = i; // 核心点3
        pool.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " id=" + id);
        });
    }
}
  • 核心点3 int id = i; :Lambda 表达式内部使用的外部变量必须是事实上的 final (effectively final)。在 for 循环中 i 是一直在变化的,如果直接在 Lambda 里写 i,编译会报错。通过 int id = i; 创建了一个局部变量,每次循环 id 都没有被重新赋值,满足了 final 的要求,从而可以安全地在 Lambda 中使用。

Lambda表达式_变量捕获

相关推荐
devilnumber2 小时前
Java 递归算法 详解 + 核心要点 + 实战运用 + 避坑指南
java·开发语言·算法
asdfg12589634 小时前
JavaBean是什么?怎么理解?有什么用途?
java·开发语言
dsyyyyy11014 小时前
JavaScript变量
开发语言·javascript·ecmascript
z落落5 小时前
C#WinForm 窗体切换与窗体传值(登录跳转案例)+WinForm 窗体传值(从上往下传、从下往上传)
开发语言·windows·c#
allway25 小时前
How to Echo Multiline to a File in Bash [3 Methods]
开发语言·chrome·bash
weixin_462446235 小时前
手把手教你用 Bash 脚本自动更新 /etc/hosts —— 自动绑定网卡 IP 与节点名
开发语言·tcp/ip·bash
一个梦醒了5 小时前
安装git bash选项推荐
开发语言·git·bash
摇滚侠6 小时前
SpringMVC 入门到实战 文件上传 75-77
java·后端·spring·maven·intellij-idea
GIS数据转换器6 小时前
城市排水生命线安全运行监测平台深度解析
java·运维·人工智能·python·安全·数据挖掘·无人机
ct9786 小时前
React 状态管理方案深度对比
开发语言·前端·react