有关线程池的一些知识点的学习笔记

有关线程池的一些知识点的学习笔记

周四 5月 22 2025
2388 字 · 12 分钟
java 未分配标签

线程池的基本概述

​ 线程池是一种对线程进行复用和管理的机制。在 Java 中,通过线程池来控制线程的数量和线程的状态,提高线程的执行效率,避免频繁创建和销毁线程带来的资源消耗。就像餐厅的传菜员。餐厅有很多顾客下的订单(任务),而传菜员(线程)负责将菜送到顾客那里。如果每个订单都单独找一个传菜员,会导致传菜员数量过多,餐厅资源紧张。所以餐厅会安排固定的传菜员团队(线程池),他们负责不断地在厨房和顾客之间来回送菜(执行任务),提高服务效率。

image-20250517144746301

线程池的状态 image-20250517144928151

状态转换如图所示: image-20250517155315553

线程池的简单使用流程

简单的使用流程可以总结为:任务提交 → 核心线程执行 → 任务队列缓存 → 非核心线程执行 → 拒绝策略处理 第一步,创建线程池。

第二步,调用线程池的 execute()方法,准备执行任务。

  • 如果正在运行的线程数量小于 corePoolSize,那么线程池会创建一个新的线程来执行这个任务;
  • 如果正在运行的线程数量大于或等于 corePoolSize,那么线程池会将这个任务放入等待队列;
  • 如果等待队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么线程池会创建新的线程来执行这个任务;
  • 如果等待队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会执行拒绝策略。

第三步,线程执行完毕后,线程并不会立即销毁,而是继续保持在池中等待下一个任务。

第四步,当线程空闲时间超出指定时间,且当前线程数量大于核心线程数时,线程会被回收。

执行流程图如下所示:image-20250517153337539

简单代码示例

JAVA
class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建一个线程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                3, // 核心线程数
                6, // 最大线程数
                0, // 线程空闲时间
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingQueue<>(10), // 等待队列
                Executors.defaultThreadFactory(), // 线程工厂
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );
        // 模拟 10 个线程办理业务
        try {
            for (int i = 1; i <= 10; i++) {
                final int tempInt = i;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t" + "办理业务" + tempInt);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

线程池的七个参数

ThreadPoolExecutor的构造方法如下所示:

JAVA
public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)

什么?白雪公主和他的七个小矮人?有点意思哈。下面讲讲每个参数的含义:

  • corePoolSize 核心线程数目

    含义:线程池中始终保留的最小线程数,即使这些线程处于空闲状态也不会被回收(除非调用 allowCoreThreadTimeOut(true)) 作用:保证在并发请求数少于等于该值时,所有任务都能立即得到线程执行,无需排队

  • maximumPoolSize 最大线程数目 核心线程数+救急线程数 含义:线程池允许的最大线程数 作用:当活动线程数达到 corePoolSize 且任务队列已满时,会继续创建新线程直至达到 maximumPoolSize;超过该阈值的新任务则 按拒绝策略处理

  • keepAliveTime 救急线程的存活时间 含义:当线程数量超过 核心线程数目 时,多余的线程在空闲超过此时长后会被回收 作用:控制线程池在负载低时的资源释放;unit 与之配合指定时间单位

  • unit 时间单位,针对救急线程--------控制救急线程生存时间对的单位 含义:keepAliveTime 参数的时间单位,取值为 TimeUnit 枚举 常用:TimeUnit.SECONDS(秒)TimeUnit.MILLISECOND(毫秒)

  • workQueue 阻塞队列 含义:用于缓存提交但尚未执行的任务的阻塞队列,类型为 BlockingQueue<Runnable> 作用:当线程数达到 corePoolSize 后,新任务会进入该队列等待空闲线程

  • threadFactory 线程工厂----可以为线程创建时起个好听的名字? 含义:用于创建新线程的工厂接口,默认实现可通过 Executors.defaultThreadFactory() 获取 作用:可定制线程名、是否守护线程、优先级等

  • handler 拒绝策略 含义:当线程池饱和(即线程数达 maximumPoolSize 且队列已满)时,对新提交任务的处理策略 常见策略:

    • AbortPolicy:抛出 RejectedExecutionException(默认)
    • CallerRunsPolicy:由调用线程执行该任务
    • DiscardPolicy:直接丢弃任务
    • DiscardOldestPolicy:丢弃队列中最旧的一个任务,再尝试提交当前任务

一些代码示例

JAVA
// 1. 固定大小线程池
ThreadPoolExecutor fixedPool = new ThreadPoolExecutor(
    5, 5,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
);

// 2. 缓存线程池
ThreadPoolExecutor cachedPool = new ThreadPoolExecutor(
    0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<>(),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

// 3. 自定义线程工厂和拒绝策略
ThreadFactory tf = runnable -> {
    Thread t = new Thread(runnable);
    t.setName("worker-" + t.getId());
    return t;
};
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
ThreadPoolExecutor customPool = new ThreadPoolExecutor(
    2, 10,
    30L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    tf, handler
);

每个参数之间关系就是:任务优先使用核心线程执行,满了进入等待队列,队列满了启用非核心线程备用,线程池达到最大线程数量后触发拒绝策略,非核心线程的空闲时间超过存活时间就被回收

线程池的阻塞队列

  1. ArrayBlockingQueue
    • 特性:基于数组、有界(需在构造时指定容量)、采用 FIFO 原则
    • 使用场景:希望严格控制队列大小,避免内存占用过高
  2. LinkedBlockingQueue
    • 特性:基于链表,可选有界或无界(默认容量为 Integer.MAX_VALUE),FIFO。
    • 使用场景:任务生产、消费速度不确定时,可使用无界队列以防过早拒绝。
  3. PriorityBlockingQueue
    • 特性:基于堆,无界,按优先级(Comparator)排序,非 FIFO
    • 使用场景:需要优先执行高优先级任务时
  4. SynchronousQueue
    • 特性:零容量队列,生产者线程插入必须等待消费者线程取走才能继续。
    • 使用场景newCachedThreadPool() 使用,适合执行大量短生命周期任务,不保留队列
JAVA
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(50);
BlockingQueue<Runnable> lq = new LinkedBlockingQueue<>();
BlockingQueue<Runnable> pq = new PriorityBlockingQueue<>();
BlockingQueue<Runnable> sq = new SynchronousQueue<>();

Executors提供的一些工厂方法:

  1. newFixedThreadPoolimage-20250517160105093
  2. newCachedThreadPoolimage-20250517160043587
  3. newSingleThreadExecutorimage-20250517160019462

线程池提交任务----submit与execute

execute 方法没有返回值,适用于不关心结果和异常的简单任务

JAVA
threadsPool.execute(new Runnable() {
    @Override public void run() {
        System.out.println("execute() 方法提交的任务");
    }
});

submit 有返回值,适用于需要获取结果或处理异常的场景

JAVA
Future<Object> future = executor.submit(harReturnValuetask);
try { Object s = future.get(); }
catch (InterruptedException e | ExecutionException e) {
    // 处理无法执行任务异常
} finally {
    // 关闭线程池 executor.shutdown();
}

线程池的关闭

可以调用线程池的shutdownshutdownNow方法来关闭线程池

调用shutdown方法,线程池的状态变为SHUTDOWN ①不会再接收新的任务 ②但是已经提交了的任务会执行完 ③shutdown方法不会阻塞调用线程的执行

调用shutdownNow方法,线程池的状态变为STOP ①不会接收新任务 ②会将队列中的任务返回 ③并用 interrupt 的方式中断正在执行的任务 注意shutdownNow不会真正终止正在运行的任务,只是给任务线程发送 interrupt 信号,任务是否能真正终止取决于线程是否响应 InterruptedException

线程池的异常处理

1、try-catch直接捕获处理异常

PLAINTEXT
executor.execute(() -> {
    try {
        System.out.println("任务开始");
        int result = 1 / 0; // 除零异常
    } catch (Exception e) {
        System.err.println("捕获异常:" + e.getMessage());
    }
});

2、使用 Future 获取异常

JAVA
Future<Object> future = executor.submit(() -> {
    System.out.println("任务开始");
    int result = 1 / 0; // 除零异常
    return result;
});

try {
    future.get();
} catch (InterruptedException | ExecutionException e) {
    System.err.println("捕获异常:" + e.getMessage());
}

3、自定义 ThreadPoolExecutor重写 afterExecute方法

JAVA
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()) {
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t != null) {
            System.err.println("捕获异常:" + t.getMessage());
        }
    }
};

executor.execute(() -> {
    System.out.println("任务开始");
    int result = 1 / 0; // 除零异常
});

4、使用 UncaughtExceptionHandler·捕获异常

JAVA
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.setThreadFactory(new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.err.println("捕获异常:" + e.getMessage());
            }
        });
        return thread;
    }
});

executor.execute(() -> {
    System.out.println("任务开始");
    int result = 1 / 0; // 除零异常
});

如果不关心任务返回值,建议使用 UncaughtExceptionHandler 如果关心任务返回值,建议使用 Future 如果想要全局捕获所有任务异常,建议重写 afterExecute 方法

来点面试题

解释 ThreadPoolExecutor 的七个参数及其作用。

当核心线程数已满且队列已满时,新任务如何处理?

LinkedBlockingQueueSynchronousQueue 的区别及适用场景。

如何自定义线程工厂与拒绝策略?请给出代码示例。

为什么要使用线程池而不是直接创建新线程?

如何监控线程池运行状态(如活跃线程数、队列大小、已完成任务数)?

allowCoreThreadTimeOut(true) 有何作用?

请简述实现一个简单线程池的思路。

在高并发场景下,如何调优 corePoolSizemaximumPoolSize 和队列容量?线程池如何调优

Executors.newFixedThreadPool()new ThreadPoolExecutor() 在底层有何区别?


Thanks for reading!

有关线程池的一些知识点的学习笔记

周四 5月 22 2025
2388 字 · 12 分钟
java 未分配标签

© TguoV | CC BY-NC-SA 4.0