Java 多线程基础-04:线程池

4 阅读9分钟

前言

1. 线程池定义

线程池(Thread Pool)是一种基于池化思想的线程管理机制。它预先创建一定数量的线程放入"池"中,当有任务需要执行时,从池中获取空闲线程来执行任务,任务执行完毕后线程不立即销毁,而是返回池中等待下一个任务。

线程的创建和销毁需要操作系统层面的资源分配和回收,涉及系统调用、内存分配、上下文初始化等操作,开销不容忽视。通过引入线程池,可以复用已经创建完成的线程,减少开销和优化管理。

线程池有诸多优势,如下给出几点参考:

优势说明
降低资源消耗复用已创建的线程,减少线程创建销毁的开销
提高响应速度任务到达时,线程已存在可直接执行
提高线程可管理性统一管理、监控和调优
提供功能扩展支持定时执行、周期执行等

2. 线程池参数

线程池在 Java 中通常由 ThreadPoolExecutor 类来实现,其构造函数提供了一些参数来配置线程池的行为。主要参数包括:

  1. corePoolSize(核心线程数):线程池中始终保持活动的线程数量,即使它们处于空闲状态也会保持存活。如果任务数量超过了核心线程数,线程池会根据需要创建新的线程。

  2. maximumPoolSize(最大线程数):线程池中允许存在的最大线程数量。当任务队列满了且线程池中的线程数达到最大线程数时,新提交的任务会被拒绝。

  3. keepAliveTime(线程空闲超时时间):线程池中的线程在空闲超过该时间后会被终止并从线程池中移除,直到线程池中的线程数量等于核心线程数。

  4. unit(时间单位):用于指定 keepAliveTime 的时间单位。

  5. workQueue(任务队列):用于保存等待执行的任务。可以是一个有界队列或者无界队列。

  6. threadFactory(线程工厂):用于创建新线程的工厂。

  7. handler(拒绝策略):用于处理任务队列满了且线程池中的线程数达到最大线程数时的情况。常见的拒绝策略包括抛出异常、丢弃任务、丢弃最旧的任务等。

这些参数可以根据实际需求来进行配置,以控制线程池的大小、行为和性能。

ThreadFactory

ThreadFactory 是一个简单的工厂接口,用于创建新线程。它允许我们自定义线程的属性,如名称、优先级、守护状态、线程组等。

@FunctionalInterface
public interface ThreadFactory {
    Thread newThread(Runnable r);
}

使用ThreadFactory的场景通常是我们需要定制化新建线程的一些属性,比如一个常用的场景就是线程命名格式化,下面给一个代码示例:

public class NamedThreadFactory implements ThreadFactory {
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
    private final ThreadGroup group;
    
    public NamedThreadFactory(String poolName) {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : 
                              Thread.currentThread().getThreadGroup();
        namePrefix = poolName + "-thread-";
    }
    
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r, 
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        // 设置为非守护线程
        if (t.isDaemon()) {
            t.setDaemon(false);
        }
        // 设置普通优先级
        if (t.getPriority() != Thread.NORM_PRIORITY) {
            t.setPriority(Thread.NORM_PRIORITY);
        }
        return t;
    }
}

使用示例:

ExecutorService executor = new ThreadPoolExecutor(
    5, 10, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new NamedThreadFactory("order-processor"),  // 使用自定义ThreadFactory
    new ThreadPoolExecutor.AbortPolicy()
);

// 提交任务
executor.submit(() -> {
    System.out.println("当前线程: " + Thread.currentThread().getName());
    // 输出:order-processor-thread-1
});

工作队列

工作队列(workQueue)是线程池中用来存储等待被执行的任务的数据结构。当线程池中的线程数量达到上限并且工作队列已满时,新提交的任务将会被放入工作队列中等待执行。工作队列的作用是缓解任务提交与任务执行之间的压力,提高系统的吞吐量和性能。

不同类型的工作队列对任务的存储和执行方式有不同的影响。以下是常见的工作队列类型及其特点:

  1. 有界队列(Bounded Queue)

    • 有界队列有固定的容量,当队列已满时,新的任务会被拒绝或触发拒绝策略。

    • 常见的有界队列包括 ArrayBlockingQueue。

  2. 无界队列(Unbounded Queue)

    • 无界队列没有固定的容量限制,可以存储任意数量的任务。

    • 当任务提交速度大于任务执行速度时,可能会导致内存溢出。

    • 常见的无界队列包括 LinkedBlockingQueue。

  3. 同步移交队列(Synchronous Queue)

    • 同步移交队列不存储任务,每个插入操作必须等待一个对应的移除操作,反之亦然。

    • 当任务提交时,必须有线程立即接收并处理,否则任务会被拒绝或触发拒绝策略。

    • 常见的同步移交队列为 SynchronousQueue。

  4. 优先级队列(Priority Queue)

    • 优先级队列根据任务的优先级来决定执行顺序,优先级高的任务会被优先执行。

    • 常见的优先级队列为 PriorityBlockingQueue。

  5. 延迟队列(Delayed Queue)

    • 延迟队列中的任务会在一定延迟时间之后才能被取出并执行。

    • 常见的延迟队列为 DelayQueue。

不同类型的工作队列适用于不同的场景,选择合适的工作队列类型可以提高线程池的性能和效率。在使用线程池时,根据任务的特性和需求选择适合的工作队列类型是很重要的。

拒绝策略

当线程池和队列都饱和时,对新任务的处理策略。

四种内置策略:

策略行为适用场景
AbortPolicy(默认)抛出RejectedExecutionException需要明确知道任务被拒绝
CallerRunsPolicy由调用线程(提交任务的线程)处理该任务不希望丢失任务,可接受降级
DiscardPolicy直接丢弃任务,无提示允许丢弃部分任务
DiscardOldestPolicy丢弃队列最前面的任务,然后重试允许丢弃旧任务,执行新任务

线程池工作流程

graph TD
    A[提交新任务] --> B{核心线程数是否已满?}
    B -->|否| C[创建核心线程执行任务]
    B -->|是| D{任务队列是否已满?}
    D -->|否| E[任务进入队列等待]
    D -->|是| F{线程数是否达到最大值?}
    F -->|否| G[创建新线程执行任务]
    F -->|是| H[执行拒绝策略]
    
    C --> I[任务执行完成]
    E --> J[队列中的任务<br>等待核心线程空闲]
    G --> I

详细步骤说明:

  1. 提交任务时,如果核心线程数未满,创建核心线程执行
  2. 如果核心线程已满,任务进入队列等待
  3. 如果队列已满且线程数未达最大值,创建新线程执行
  4. 如果队列和线程数都达到上限,执行拒绝策略

3. 常见线程池

在介绍Java常见线程池之前,首先需要了解下Executor框架。Java 5.0引入了Executor框架,将任务的提交与执行解耦:

// 使用Executor框架
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
    doTask();  // 提交任务,不关心线程创建细节
});

框架核心接口层次:

Executor(基础接口)
  ↓
ExecutorService(增强接口:生命周期管理)
  ↓
AbstractExecutorService(抽象实现)
  ↓
ThreadPoolExecutor(核心实现类)

FixedThreadPool

// 创建固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);

// 等同于
new ThreadPoolExecutor(
    10,  // corePoolSize = maximumPoolSize
    10,  // 固定大小
    0L,  // keepAliveTime = 0(不回收)
    TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>()  // 无界队列
);

特点:

  • 线程数量固定,不会自动扩缩容
  • 使用无界队列,不会触发拒绝策略(除非内存耗尽)
  • 适用于负载较重的服务器,需要限制线程数量

CachedThreadPool

// 创建缓存线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

// 等同于
new ThreadPoolExecutor(
    0,   // corePoolSize = 0(无核心线程)
    Integer.MAX_VALUE,  // 最大线程数极大
    60L, // 空闲60秒后回收
    TimeUnit.SECONDS,
    new SynchronousQueue<>()  // 直接移交队列
);

特点:

  • 线程数量几乎无限制(Integer.MAX_VALUE)
  • 空闲线程60秒后自动回收
  • 使用SynchronousQueue,无任务缓冲
  • 适合大量短生命周期的异步任务

风险提示:

// 危险示例:可能创建大量线程
for (int i = 0; i < 1000000; i++) {
    cachedThreadPool.submit(() -> {
        // 短任务
    });
    // 可能创建大量线程导致OOM!
}

SingleThreadExecutor

// 创建单线程线程池
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();

// 等同于
new ThreadPoolExecutor(
    1,  // 只有一个线程
    1,  // 固定一个线程
    0L,
    TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(),  // 无界队列
    new ThreadPoolExecutor.DiscardPolicy()  // 特殊拒绝策略
);

特点:

  • 只有一个工作线程
  • 任务按提交顺序执行(FIFO)
  • 保证所有任务顺序执行,不需要额外同步
  • 线程异常终止后会创建新线程继续执行

ScheduledThreadPool

// 创建定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(5);

// 等同于(特殊实现)
new ScheduledThreadPoolExecutor(5);

特点:

  • 支持定时和周期性任务执行
  • 核心线程数可配置,最大线程数为Integer.MAX_VALUE
  • 使用DelayedWorkQueue作为任务队列

常用方法:

// 1. 延迟执行
scheduledPool.schedule(() -> {
    System.out.println("5秒后执行");
}, 5, TimeUnit.SECONDS);

// 2. 固定频率执行(从任务开始时间计算)
scheduledPool.scheduleAtFixedRate(() -> {
    System.out.println("每3秒执行一次,固定频率");
}, 0, 3, TimeUnit.SECONDS);

// 3. 固定延迟执行(从任务结束时间计算)
scheduledPool.scheduleWithFixedDelay(() -> {
    System.out.println("任务结束后延迟2秒再执行");
}, 0, 2, TimeUnit.SECONDS);

WorkStealingPool

// Java 8+ 引入
ExecutorService workStealingPool = Executors.newWorkStealingPool();

// 可指定并行级别
ExecutorService workStealingPool = Executors.newWorkStealingPool(8);

特点:

  • 基于ForkJoinPool实现
  • 使用工作窃取(Work-Stealing)算法
  • 适合计算密集型任务
  • 自动利用所有可用的处理器核心

工作窃取算法原理:

// 每个工作线程维护自己的双端队列
// 当自己的队列为空时,可以从其他线程队列的"尾部"窃取任务
// 减少了线程间的竞争,提高了CPU利用率

4. 线程池中断

线程池的中断主要有两个方法:shutdown()shutdownNow()

public void shutdown() {
    // 1. 将线程池状态设置为 SHUTDOWN
    // 2. 不再接受新任务提交
    // 3. 继续执行已提交的任务(包括正在执行的和队列中的)
    // 4. 不会强制中断正在执行的任务
}

public List<Runnable> shutdownNow() {
    // 1. 将线程池状态设置为 STOP
    // 2. 不再接受新任务提交
    // 3. 尝试中断所有正在执行的任务(调用 Thread.interrupt())
    // 4. 清空任务队列,返回未执行的任务列表
    // 5. 不保证一定能中断所有线程
}

使用线程池的shutdownNow不一定会真的将线程直接停止,这个取决于线程的状态以及线程对中断的处理,这一点可以参考线程的中断响应。这里给出简单回顾:

public class ThreadStateInterruptDemo {
    
    public static void testDifferentStates() {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // 任务1:RUNNABLE状态(纯计算,不检查中断)
        executor.execute(() -> {
            System.out.println("任务1开始(计算密集型)");
            long sum = 0;
            for (long i = 0; i < 1000000000L; i++) {
                sum += i;
                // 不检查中断,不会被中断
            }
            System.out.println("任务1完成,sum=" + sum);
        });
        
        // 任务2:TIMED_WAITING状态(sleep)
        executor.execute(() -> {
            System.out.println("任务2开始(sleep)");
            try {
                Thread.sleep(5000);
                System.out.println("任务2正常完成");
            } catch (InterruptedException e) {
                System.out.println("任务2被中断");
            }
        });
        
        // 任务3:BLOCKED状态(等待锁)
        Object lock = new Object();
        synchronized (lock) {
            executor.execute(() -> {
                System.out.println("任务3开始(等待锁)");
                synchronized (lock) {  // 会阻塞在这里
                    System.out.println("任务3获取到锁");
                }
                System.out.println("任务3完成");
            });
            
            // 让任务3先启动并进入BLOCKED状态
            try { Thread.sleep(100); } catch (InterruptedException e) {}
        }
        
        // 任务4:WAITING状态(wait)
        executor.execute(() -> {
            System.out.println("任务4开始(wait)");
            synchronized (lock) {
                try {
                    lock.wait(5000);
                    System.out.println("任务4正常完成");
                } catch (InterruptedException e) {
                    System.out.println("任务4被中断");
                }
            }
        });
        
        // 给任务一点时间进入各自的状态
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        
        System.out.println("\n调用 shutdownNow()...");
        List<Runnable> pending = executor.shutdownNow();
        System.out.println("未执行的任务数:" + pending.size());
        
        try {
            executor.awaitTermination(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

如果希望能够精准的取消某个线程的执行,可以使用Future.cancel(true) 函数。