深入理解线程池

一 线程池的定义

线程池(Thread Pool)是一种设计模式,它通过预先创建并维护一组可重用的线程来执行任务。这些线程被集中管理在一个池中,当有任务需要执行时,线程池会从池中分配一个空闲线程来处理任务,而不是为每个任务都创建新的线程。任务完成后,线程不会被销毁,而是返回到池中等待下一个任务。

二 线程池的目的

线程池的主要目的是优化并发任务的执行效率,同时提高系统的稳定性和资源利用率。具体来说,线程池的设计旨在解决以下问题:

2.1 减少线程创建和销毁的开销

  • 每次创建新线程都会消耗系统资源(如内存、CPU等),并且销毁线程也会带来额外的开销。线程池通过复用已有的线程,避免了频繁的线程创建和销毁操作。

2.2 控制并发线程的数量

  • 如果允许无限制地创建线程,可能会导致系统资源耗尽,甚至引发崩溃。线程池通过限制线程的数量,防止过多线程同时运行,从而避免资源竞争和系统过载。

2.3 提高任务响应的速度

  • 预先创建的线程可以立即执行任务,无需等待线程初始化的时间,从而显著提升任务的响应速度。

2.4 简化线程管理

  • 线程池提供了一种统一的方式来管理和调度线程,开发者无需手动创建和销毁线程,降低了编程复杂度。

三 线程池的实现

3.1 固定大小线程池(Fixed Thread Pool)

  • 实现方式:通过Executors.newFixedThreadPool(int nThreads)方法创建。
  • 特点:拥有固定数量的工作线程。如果所有线程都在忙于执行任务,额外的任务将会排队等待,直到有线程空闲出来。
  • 适用场景:适用于负载较为稳定的应用场景,可以控制并发线程的数量以避免资源耗尽。

3.2 缓存线程池(Cached Thread Pool)

  • 实现方式:使用Executors.newCachedThreadPool()方法创建。
  • 特点:根据需要动态创建新线程,并且可以重用空闲线程。如果长时间没有使用的线程会被终止并从缓存中移除。
  • 适用场景:适合执行大量短生命周期的异步任务,能够高效利用系统资源。

3.3 单一线程池(Single Thread Executor)

  • 实现方式:通过Executors.newSingleThreadExecutor()方法创建。
  • 特点:保证任意时间点只有一个线程是活动的,确保任务按顺序执行。
  • 适用场景:当需要保证任务顺序执行时非常有用,比如处理关键事务或需要保证操作顺序一致性的任务。

3.4 周期性线程池(Scheduled Thread Pool)

  • 实现方式:使用Executors.newScheduledThreadPool(int corePoolSize)方法创建。
  • 特点:支持定时及周期性任务执行。可以通过指定延迟时间来延迟任务执行,也可以设定固定的频率重复执行任务。
  • 适用场景:适合需要调度任务的场景,如定时备份、数据同步等。

3.5 工作窃取线程池(Work Stealing Thread Pool)

  • 实现方式:通过new ForkJoinPool()方法创建。
  • 特点:采用工作窃取算法,线程池中的每个线程都有自己的任务队列,当一个线程完成了自己队列中的任务后,可以从其他线程的队列末尾“窃取”任务来执行,从而提高线程利用率和任务处理效率。
  • 适用场景:特别适合可以分解为多个子任务的大任务,如并行计算、大数据处理等场景。

四 线程池的核心参数

线程池的核心参数主要涉及其配置选项,这些参数决定了线程池的行为、性能以及资源管理方式。在Java中,ThreadPoolExecutor类允许开发者通过设置不同的参数来自定义线程池的行为。以下是几个关键的核心参数:

4.1 核心线程数(corePoolSize) 

  • 定义:线程池中保持的最小线程数量,即使这些线程处于空闲状态,除非设置了允许核心线程超时。
  • 作用:决定线程池中最少有多少个线程随时待命以处理新任务。

4.2 最大线程数(maximumPoolSize)

  • 定义:线程池中允许的最大线程数。
  • 作用:当任务量增加时,如果当前运行的线程数少于maximumPoolSize,则会创建新的线程来处理任务,直到达到最大限制。

4.3 线程空闲时间(keepAliveTime) 

  • 定义:当线程数超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
  • 作用:控制非核心线程的存活时间,有助于减少系统资源占用,特别是在负载波动较大的场景下。

4.4 时间单位(unit)

  • 定义:为keepAliveTime指定的时间单位,可以是纳秒、微秒、毫秒、秒、分钟、小时或天。
  • 作用:精确地定义线程空闲时间的长度。

4.5 工作队列(workQueue)

1. SynchronousQueue

  • 特性:不存储元素的阻塞队列,每个插入操作必须等待一个移除操作,反之亦然。
  • 适用场景:适用于任务提交速率较高且希望直接将任务分配给线程执行的情况。如果当前没有可用线程来处理任务,则会创建新线程(直到达到最大线程数限制)。适合于需要立即处理任务而不需要排队的场景。

2. LinkedBlockingQueue

  • 特性:基于链表结构的无界阻塞队列(可以通过构造函数指定容量成为有界队列),按照先进先出(FIFO)原则对元素进行排序。
  • 适用场景:由于其无界特性(除非特别指定了容量),可以用来处理大量任务而不会因为队列满而导致任务被拒绝。但是需要注意的是,无界队列可能会导致内存溢出。

3. ArrayBlockingQueue

  • 特性:由数组支持的有界阻塞队列,同样遵循FIFO原则。与LinkedBlockingQueue不同,它的大小是固定的,一旦初始化就不能改变。
  • 适用场景:适用于需要限制并发任务数量的应用场景,可以帮助控制系统的负载,避免资源耗尽。

4. PriorityBlockingQueue

  • 特性:一个支持优先级排序的无界阻塞队列。元素按照自然顺序或者通过提供的Comparator进行排序。
  • 适用场景:当需要根据任务的优先级来决定执行顺序时使用,例如实现定时器、任务调度等场景。

5. DelayQueue

  • 特性:一个无界阻塞队列,其中的元素只有在其延迟期满时才能被取出。队列中的元素必须实现Delayed接口。
  • 适用场景:非常适合用于实现带有超时特性的任务,如缓存系统中的过期键值对清除、定时任务调度等。

4.6 拒绝策略(handler) 

当线程池和工作队列都已满时,对于新提交的任务采取的策略。 

  • 常见策略
    • AbortPolicy(默认):直接抛出RejectedExecutionException异常。
    • CallerRunsPolicy:由调用线程(提交任务的线程)执行该任务。
    • DiscardPolicy:直接丢弃任务,不做任何处理。
    • DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试重新提交当前任务。

五 实现一个简单的线程池 

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 自定义线程池实现类。
 * 该类通过维护一个阻塞队列来管理任务,并使用固定数量的线程来执行这些任务。
 */
class MyThreadPool {

    // 定义一个阻塞队列,用于存储待执行的任务。
    private BlockingQueue queue = new LinkedBlockingQueue<>();

    /**
     * 提交任务到线程池的方法。
     * 将任务添加到阻塞队列中。如果队列已满(如果是有界队列),则会阻塞当前线程直到有空间可用。
     *
     * @param runnable 要提交的任务(实现了 Runnable 接口的对象)。
     * @throws InterruptedException 如果在等待队列空间时线程被中断,则抛出异常。
     */
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable); // 将任务放入队列中。
    }

    /**
     * 构造方法,初始化线程池。
     * 创建指定数量的工作线程,每个线程会从阻塞队列中取出任务并执行。
     *
     * @param n 线程池中工作线程的数量。
     */
    public MyThreadPool(int n) {
        // 根据指定的数量创建多个线程。
        for (int i = 0; i < n; i++) {
            // 创建一个新的线程,该线程会不断从队列中取出任务并执行。
            Thread t = new Thread(() -> {
                try {
                    while (true) { // 无限循环,持续从队列中取任务。
                        // 从阻塞队列中取出一个任务(如果队列为空,则线程会阻塞等待)。
                        Runnable runnable = queue.take();
                        // 执行任务的 run 方法。
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    // 捕获线程中断异常,并打印堆栈信息。
                    e.printStackTrace();
                }
            });
            // 启动线程,使其开始运行。
            t.start();
        }
    }
}

你可能感兴趣的:(Java,java,java-ee)