Java中的几种线程池

线程

生命周期

何时我们会考虑使用线程池?

为什么用线程池?

线程的创建/销毁开销大,频繁创建线程会:

  • 创建传统的线程将消耗大量系统资源(每个线程默认占用 512KB~1MB 栈空间)
  • 导致频繁的上下文切换,降低 CPU 利用率
  • 在高并发场景下可能因线程数失控导致 OOM
  • 异步解耦:下单后异步发短信/推送通知,主流程不阻塞
  • 批量并行处理:批量调用第三方 API、并行查多个数据源(配合 CompletableFuture
  • IO 密集型服务:HTTP 请求处理、数据库查询,线程大部分时间在等待,可以开较多线程
  • CPU 密集型计算:图像处理、报表生成,线程数建议 ≈ CPU 核心数
  • 定时/周期任务:心跳检测、缓存刷新,用 ScheduledThreadPoolExecutor

参数设置:

IO 密集型:corePoolSize = 2 × CPU核心数

CPU 密集型:corePoolSize = CPU核心数 + 1

关于 Executors

Executors 提供的四种标准线程池

线程池核心线程最大线程队列适用场景
newFixedThreadPool(n)nnLinkedBlockingQueue(无界)负载稳定、任务量可预估
newCachedThreadPool()0Integer.MAX_VALUESynchronousQueue大量短时任务、突发流量
newSingleThreadExecutor()11LinkedBlockingQueue(无界)需要严格串行执行
newScheduledThreadPool(n)nInteger.MAX_VALUEDelayedWorkQueue定时任务、周期任务

⚠️ 阿里规范禁止直接用 Executors 工厂方法:Fixed/Single 的无界队列可能堆积大量任务导致 OOMCached 的线程数无上限同样危险。生产环境请直接用 ThreadPoolExecutor


关于 ThreadPoolExecutor

new ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数(常驻)
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程空闲存活时间
    TimeUnit unit,
    BlockingQueue workQueue, // 任务队列
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler  // 拒绝策略
);
```

**执行流程:**
```
提交任务
  → 核心线程未满?→ 创建核心线程执行
  → 队列未满?    → 入队等待
  → 最大线程未满?→ 创建非核心线程执行
  → 触发拒绝策略

四种拒绝策略:

策略行为
AbortPolicy(默认)抛出 RejectedExecutionException
CallerRunsPolicy由调用方线程直接执行,起到限流效果
DiscardPolicy静默丢弃
DiscardOldestPolicy丢弃队列头部最旧的任务

关于虚拟线程

创建虚拟线程

Executors.newVirtualThreadPerTaskExecutor()

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10000; i++) {
        executor.submit(() -> {
            // 每个任务独占一个虚拟线程
            Thread.sleep(Duration.ofMillis(100));
            System.out.println(Thread.currentThread());
        });
    }
} // try-with-resources 自动 shutdown

注意事项

① 避免池化虚拟线程

// ❌ 错误:用固定线程池包装虚拟线程,失去意义
ExecutorService wrong = Executors.newFixedThreadPool(
    100, Thread.ofVirtual().factory()
);

② 避免 ThreadLocal 滥用
虚拟线程数量庞大,ThreadLocal 存储大对象会造成内存压力,考虑用 ScopedValue(JDK 21 preview)替代。

③ CPU 密集型任务仍用平台线程池
虚拟线程的优势在于 IO 等待时自动让出载体线程。纯计算任务用虚拟线程没有收益,反而增加调度开销。

④ 小心 synchronized 导致的 pinning
虚拟线程在 synchronized 块内阻塞时会**固定(pin)**载体线程,导致载体线程无法被其他虚拟线程使用。推荐改用 ReentrantLock

// ❌ 可能 pin 住载体线程
synchronized (lock) {
    doBlockingIO();
}

// ✅ 推荐
lock.lock();
try {
    doBlockingIO();
} finally {
    lock.unlock();
}

关于 ScheduledExecutorService

ScheduledExecutorService 核心接口

public interface ScheduledExecutorService extends ExecutorService {

    // 延迟一次性执行
    ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);

    // 固定频率:上次【开始】时间 + period 后触发下次
    ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                           long initialDelay,
                                           long period,
                                           TimeUnit unit);

    // 固定延迟:上次【结束】时间 + delay 后触发下次
    ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                              long initialDelay,
                                              long delay,
                                              TimeUnit unit);
}
```

---

### `AtFixedRate` vs `WithFixedDelay`
```
AtFixedRate(period = 2s,任务耗时 1s):
  |--task--|  wait  |--task--|  wait  |--task--|
  0        1        2        3        4        5

AtFixedRate(period = 2s,任务耗时 3s,超时):
  |-----task-----|--task-----|-----task-----|
  0              3           6              9
  ← 超时后不等待,上次结束立即触发下次 →

WithFixedDelay(delay = 2s,任务耗时 1s):
  |--task--|--2s--|--task--|--2s--|--task--|
  0        1      3        4      6        7
  ← delay 从任务结束后开始计算 →

选择原则:

  • 需要严格按时间点触发(如整点报表)→ AtFixedRate
  • 需要保证两次任务之间有足够间隔(如轮询外部接口)→ WithFixedDelay

关键细节:异常会静默吞掉任务

这是最常见的坑:

scheduler.scheduleAtFixedRate(() -> {
    // 一旦抛出未捕获异常,任务会永久停止调度
    // 没有任何报错提示!
    riskyOperation();
}, 0, 1, TimeUnit.MINUTES);

必须在任务内部捕获异常:

java

scheduler.scheduleAtFixedRate(() -> {
    try {
        riskyOperation();
    } catch (Exception e) {
        log.error("定时任务执行异常", e);
        // 异常被消化,调度继续
    }
}, 0, 1, TimeUnit.MINUTES);

取消与关闭

java

// 取消单个任务
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(...);
future.cancel(false);  // false = 不中断正在执行的任务
                       // true  = 中断正在执行的任务

// 关闭线程池(两种方式)
scheduler.shutdown();            // 等待已提交任务执行完
scheduler.shutdownNow();         // 立即中断,返回未执行任务列表

线程数设置建议

ScheduledThreadPool 的线程数 ≠ 并发执行数,而是同时可执行的任务数。

// 如果同一时刻最多有 N 个定时任务可能并发触发
// 线程数设为 N 即可,不需要过多
Executors.newScheduledThreadPool(N);

Future 接口

Future 是一个接口。代表一个异步计算的结果,表示一个可能还没有完成的任务最终会完成的计算。

// Future 的位置在 java.util.concurrent 包
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

核心思想

  • 当你向线程池提交一个任务时,线程池返回一个 Future 对象
  • 这个对象就代表这个任务的未来结果
  • 你可以通过这个对象查询任务状态、获取结果、取消任务

常用方法

  • get(5,TimeUnit.SECONEDS); 阻塞当前线程,直到异步任务完成,返回任务的计算结果。
    如果任务抛出异常,get() 会抛出 ExecutionException
    如果当前线程被中断,抛出 InterruptedException
    无超时的 get() 操作是危险的,可能导致永久阻塞。
  • boolean isDone();如果任务已完成,返回 true (正常完成或异常或被取消)。
    非阻塞式查询。
  • boolean cancel(boolean mayInterruptIfRunning);取消任务。mayInterruptIfRunning = true:如果任务正在运行,尝试中断它1mayInterruptIfRunning = false:只有在任务还未开始时才取消,如果任务已开始则不取消。
  • boolean isCancelled();
    返回 true 如果任务被成功取消。
    返回 false 如果任务未被取消或已完成。

  1. 中断真正成功的两条路:
    路径1:任务在阻塞操作中路经2:任务主动检查中断标记。(如果不满足,任务并不会被中断。) ↩︎

后续将更新CompletableFuture
No Comments

Send Comment Edit Comment


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
Previous
Next