线程池是一种管理多个线程的工具,用于优化线程的使用,避免频繁创建和销毁线程带来的性能损耗。它通过复用线程来提高系统资源利用率和任务处理效率,是 Java 并发编程中重要的部分。

线程池的核心思想

  1. 复用线程

    • 通过维护一个线程集合(线程池),在有任务时分配线程,无任务时线程处于等待状态。
  2. 减少创建和销毁线程开销

    • 避免频繁创建和销毁线程,降低资源开销。
  3. 任务排队

    • 当所有线程都忙时,将任务放入队列等待。

Java 中的线程池

Java 提供了 java.util.concurrent 包下的线程池实现,主要通过 Executor 框架来管理。

核心类和接口

  1. Executor

    • 线程池顶层接口,定义了基本的任务提交方法。
  2. ExecutorService

    • 扩展了 Executor,增加了管理线程池生命周期的方法,如 shutdown()、awaitTermination()。
  3. ThreadPoolExecutor

    • 线程池的具体实现类,提供了高度可配置的线程池
  4. Executors

    • 工具类,用于创建常见类型的线程池。

工作流程

  1. 当任务提交到线程池

    • 如果有空闲线程,任务直接交由空闲线程执行
    • 如果没有空闲线程,且线程池并未到达最大值,创建新的线程执行任务
    • 如果线程数到达最大值,直接进入等待队列
    • 如果等待队列也满了,根据策略处理任务(如拒绝任务,抛出异常)

核心参数

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数
  3. keepAliveTime:空闲线程的存活时间,多余线程在此时间后销毁
  4. workQueue:用于存储任务的等待队列
  5. threadFactory:创建线程的工厂类,通常用于定义线程属性
  6. hanlder:拒绝策略

常见的线程池类型

通过Executors工具类可以快速创建以下线程池:

  1. 固定线程池(newFixedThreadPool)

    • 固定数量的线程,适用于稳定负载。
    • 示例:ExecutorService executor = Executors.newFixedThreadPool(5);
  2. 缓存线程池(newCachedThreadPool)

    • 动态调整线程数量,适用于大量短时任务
    • 示例:ExecutorService executor = Executors.newCachedThreadPool();
  3. 单线程池(newSingleThreadExecutor)

    • 单线程串行执行任务。
    • 示例:ExecutorService executor = Executors.newSingleThreadExecutor();
  4. 定时线程池(newScheduledThreadPool)

    • 执行延迟或周期性任务。
    • 示例:ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);

自定义线程池

import java.util.concurrent.*;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        // 自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2, // 核心线程数
                5, // 最大线程数
                1, TimeUnit.MINUTES, // 空闲线程存活时间
                new LinkedBlockingQueue<>(10), // 队列容量
                Executors.defaultThreadFactory(), // 线程工厂
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );

        for (int i = 0; i < 20; i++) {
            int task = i;
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is executing task " + task);
            });
        }

        threadPool.shutdown();
    }
}

线程池优点

  1. 降低资源消耗
    通过复用线程减少频繁创建和销毁线程的开销。
  2. 提高响应速度
    任务可以直接复用现有线程处理,无需等待线程创建。
  3. 提升管理能力
    可以限制线程数量,避免因资源争抢导致的系统崩溃。

线程池的注意事项

  1. 任务提交后无法取消
  2. 阻塞队列的选择

    • 有界队列需配合拒绝策略处理超出容量的任务
    • 无界队列(LinkedBlockingQueue)可能导致最大线程数失效
  3. 避免任务执行时间过长
    任务执行时间过长会阻塞线程,降低线程池吞吐量。
  4. 避免资源泄漏
    使用 shutdown() 及时关闭线程池。

线程池拒绝策略

当线程池无法接收新任务时(如队列满,线程池关闭),会触发拒绝策略。Java 提供了以下内置策略:

  1. AbortPolicy
    抛出 RejectedExecutionException。
  2. CallerRunsPolicy
    由调用线程执行任务。
  3. DiscardPolicy
    丢弃任务,不抛异常。
  4. DiscardOldestPolicy
    丢弃队列中最旧的任务,然后尝试重新提交
import java.util.concurrent.*;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);

        Runnable task = () -> System.out.println("Task executed at: " + System.currentTimeMillis());

        // 延迟 2 秒后执行
        scheduler.schedule(task, 2, TimeUnit.SECONDS);

        // 延迟 1 秒后,每隔 3 秒执行一次
        scheduler.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);

        // 延迟 1 秒后,间隔 3 秒执行任务,但以任务执行结束时间为间隔
        scheduler.scheduleWithFixedDelay(task, 1, 3, TimeUnit.SECONDS);
    }
}

向线程池提交任务的方式

常用的方式包括提交可运行的任务(Runnable)、可调用的任务(Callable)、批量任务以及定时任务。

  1. 使用 execute() 方法提交任务,不关心任务的返回结果
  2. 使用 submit() 方法提交任务,提交任务后会返回一个 Future 对象,可通过它获取任务的执行结果或监控任务状态。支持 Runnable 和 Callable 两种任务类型
  3. 批量提交:使用线程池的 invokeAll() 和 invokeAny() 方法一次性提交多个 Callable 任务。

    • invokeAll():返回所有任务的 Future 对象,阻塞直到所有任务完成。
    • invokeAny():返回最快完成任务的结果,其他任务取消。
  4. 提交定时任务:使用 ScheduledExecutorService 提交定时或周期性任务。

关闭线程池

  1. shutdown()

    • 发出线程池的关闭请求,不再接受新任务,但会继续执行已经提交的任务,包括在队列中的任务。
    • 线程池中的线程在完成所有任务后会被销毁
    • 注意:

      • 如果需要确保线程池已完全关闭,可以调用 awaitTermination() 进行阻塞等待
  2. shutdownNow()

    • 试图停止正在执行的任务并返回未执行的任务列表,同时不再接受新任务
    • 使用此方法可能会导致任务中断,但具体是否中断取决于任务代码的设计(是否响应中断)。

线程池如何合理设置

理解任务类型

CPU密集型任务

  • 特点:主要使用 CPU 进行计算,例如科学计算、加密解密、图像处理等
  • 推荐大小:线程池大小应接近于 CPU 核心数。
  • 公式:CPU+1

I/O密集型任务

  • 特点:任务包含大量 I/O 操作,例如网络请求、文件读写等,线程通常处于等待状态。
  • 推荐大小:线程池大小应远大于 CPU 核心数
  • 原因:增加线程数可以充分利用等待时间,提高吞吐量。

混合型任务

  • 同时包含 CPU 密集和 I/O 密集操作。
  • 解决方法:拆分任务为 CPU 密集型和 I/O 密集型,分别用不同线程池处理。
系统资源约束
  • 内存消耗:线程池中的每个线程会占用一定的内存(主要是栈空间,通常为1MB),过多的线程可能导致内存溢出。
  • 上下文切换:线程数过多会增加上下文切换开销,导致性能下降。
使用任务队列管理任务

线程池的任务队列可以配置不同类型来影响线程池的行为:

直接提交队列(SynchonousQueue)

  • 特点:每次提交任务都需要有线程立即处理,否则无法提交。

    • 适用场景:需要严格控制线程数,任务执行速度与线程池速度匹配。

无界队列(LinkedBlockingQueue)

  • 特点:队列大小无限制,新任务可以持续进入,线程数固定。

    • 适用场景:任务执行时间短,吞吐量大。

有界队列(ArrayBlockingQueue)

  • 队列大小固定,超出时需要等待或拒绝任务。

    • 需要限制内存占用,防止任务堆积

注意事项

  • 避免死锁:设置合理的队列大小和线程数,防止任务相互依赖导致死锁。
  • 选择拒绝策略:

    • AbortPolicy:抛出异常。
    • CallerRunsPolicy:在提交任务的线程中执行任务。
    • DiscardPolicy:丢弃任务,不抛异常。
    • DiscardOldestPolicy:丢弃最早的任务。
  • 测试和优化:通过负载测试不断调整线程池大小。

爱跑步的猕猴桃
1 声望0 粉丝

引用和评论

0 条评论