MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java 线程池核心参数详解

2021-09-073.8k 阅读

Java 线程池核心参数详解

在 Java 并发编程中,线程池是一种非常重要的工具,它可以有效地管理和复用线程,提高应用程序的性能和资源利用率。要深入理解线程池的工作原理,就必须了解其核心参数。下面我们将详细介绍 Java 线程池的核心参数,并通过代码示例进行说明。

1. corePoolSize(核心线程数)

  • 含义:线程池中核心线程的数量。所谓核心线程,是指在没有任务时也不会被销毁的线程,除非设置了 allowCoreThreadTimeOuttrue。核心线程会一直存活,等待任务到来。
  • 作用:这个参数决定了线程池在正常情况下维护的线程数量。当有新任务提交到线程池时,如果当前线程数小于 corePoolSize,线程池会创建新的线程来执行任务,即使其他线程处于空闲状态。这样可以保证在有任务时,能快速响应并处理任务,避免频繁创建和销毁线程带来的开销。

代码示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CorePoolSizeExample {
    public static void main(String[] args) {
        // 创建一个固定核心线程数为 2 的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        for (int i = 0; i < 5; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskNumber + " is being processed by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executorService.shutdown();
    }
}

在上述示例中,Executors.newFixedThreadPool(2) 创建了一个核心线程数为 2 的线程池。当提交 5 个任务时,线程池会先创建 2 个核心线程来处理前 2 个任务,剩下的 3 个任务会进入队列等待核心线程处理完当前任务后再执行。

2. maximumPoolSize(最大线程数)

  • 含义:线程池中允许存在的最大线程数。当任务队列已满,并且当前线程数小于 maximumPoolSize 时,线程池会创建新的非核心线程来处理任务。
  • 作用:这个参数限制了线程池所能容纳的最大线程数量,防止线程无限制地创建,从而避免系统资源耗尽。在高并发场景下,如果任务数量突然剧增,超过了核心线程数和队列的承载能力,线程池会创建额外的线程(最多达到 maximumPoolSize)来处理任务,以保证系统的响应性。

代码示例

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MaximumPoolSizeExample {
    public static void main(String[] args) {
        // 创建一个线程池,核心线程数为 2,最大线程数为 4,队列容量为 3
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                2,
                4,
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3)
        );

        for (int i = 0; i < 10; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskNumber + " is being processed by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executorService.shutdown();
    }
}

在这个示例中,线程池的核心线程数为 2,最大线程数为 4,队列容量为 3。当提交 10 个任务时,首先 2 个任务由核心线程处理,3 个任务进入队列,然后再创建 2 个非核心线程(因为 2 + 3 < 10 且当前线程数 2 小于最大线程数 4)来处理剩下的 5 个任务中的 2 个,最后剩下 3 个任务等待前面的线程处理完任务后依次执行。

3. keepAliveTime(线程存活时间)

  • 含义:当线程池中的线程数量大于 corePoolSize 时,多余的非核心线程如果空闲时间超过 keepAliveTime,则会被销毁。当 allowCoreThreadTimeOut 设置为 true 时,核心线程空闲时间超过 keepAliveTime 也会被销毁。
  • 作用:这个参数用于控制非核心线程(或核心线程,如果 allowCoreThreadTimeOuttrue)在空闲状态下的存活时间,避免过多空闲线程占用系统资源。通过合理设置 keepAliveTime,可以在高并发任务结束后,自动减少线程池中的线程数量,从而降低系统资源消耗。

代码示例

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class KeepAliveTimeExample {
    public static void main(String[] args) {
        // 创建一个线程池,核心线程数为 2,最大线程数为 4,线程存活时间为 5 秒
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                2,
                4,
                5,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        for (int i = 0; i < 5; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskNumber + " is being processed by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("After 6 seconds, pool size: " + executorService.getPoolSize());

        executorService.shutdown();
    }
}

在上述代码中,线程池的核心线程数为 2,最大线程数为 4,线程存活时间为 5 秒。当提交 5 个任务时,会创建 2 个核心线程和 2 个非核心线程。任务执行完后,等待 6 秒,由于非核心线程空闲时间超过了 5 秒,非核心线程会被销毁,此时线程池大小为 2(核心线程数)。

4. unit(时间单位)

  • 含义keepAliveTime 的时间单位。它可以是 TimeUnit 枚举类中的各种时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)、TimeUnit.MINUTES(分钟)等。
  • 作用:明确 keepAliveTime 的时间度量单位,使得时间设置更加清晰和准确。例如,如果希望线程存活时间为 1000 毫秒,使用 TimeUnit.MILLISECONDS 作为时间单位,设置 keepAliveTime 为 1000 就很直观;如果使用 TimeUnit.SECONDS,则需要将 keepAliveTime 设置为 1。

代码示例

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class UnitExample {
    public static void main(String[] args) {
        // 创建一个线程池,核心线程数为 2,最大线程数为 4,线程存活时间为 1 分钟
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                2,
                4,
                1,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>()
        );

        // 提交任务等操作...

        executorService.shutdown();
    }
}

在这个示例中,通过 TimeUnit.MINUTES 明确了 keepAliveTime 的时间单位为分钟,这样就清楚地表示线程的存活时间为 1 分钟。

5. workQueue(任务队列)

  • 含义:用于存储等待执行的任务的队列。当提交的任务数量超过核心线程数时,新任务会被放入这个队列中等待处理。
  • 作用:任务队列在调节线程池的任务处理能力方面起着关键作用。它可以缓冲任务,避免在瞬间高并发时无限制地创建线程。不同类型的任务队列有不同的特性,选择合适的任务队列对于线程池的性能和稳定性至关重要。常见的任务队列类型有:
    • ArrayBlockingQueue:基于数组的有界阻塞队列,按 FIFO(先进先出)原则对元素进行排序。队列的大小在创建时就固定下来,一旦达到队列容量,新的任务就无法再加入队列。
    • LinkedBlockingQueue:基于链表的无界阻塞队列(也可以指定容量变为有界队列),按 FIFO 原则对元素进行排序。由于它是无界的(默认情况下),理论上可以容纳无限个任务,因此在使用时要特别注意,防止任务大量堆积导致内存溢出。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等待另一个线程的移除操作,反之亦然。它适用于传递性场景,即任务快速传递,不希望任务在队列中等待,而是直接交给线程处理。
    • PriorityBlockingQueue:基于堆结构的无界阻塞队列,元素按照自然顺序或自定义顺序进行排序。任务在队列中会根据优先级顺序执行,适用于对任务执行顺序有要求的场景。

代码示例

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class WorkQueueExample {
    public static void main(String[] args) {
        // 使用 ArrayBlockingQueue,队列容量为 3
        ThreadPoolExecutor executorWithArrayQueue = new ThreadPoolExecutor(
                2,
                4,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3)
        );

        // 使用 LinkedBlockingQueue,无界队列
        ThreadPoolExecutor executorWithLinkedQueue = new ThreadPoolExecutor(
                2,
                4,
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 使用 SynchronousQueue
        ThreadPoolExecutor executorWithSyncQueue = new ThreadPoolExecutor(
                2,
                4,
                10,
                TimeUnit.SECONDS,
                new SynchronousQueue<>()
        );

        // 使用 PriorityBlockingQueue
        ThreadPoolExecutor executorWithPriorityQueue = new ThreadPoolExecutor(
                2,
                4,
                10,
                TimeUnit.SECONDS,
                new PriorityBlockingQueue<>()
        );

        // 提交任务等操作...

        executorWithArrayQueue.shutdown();
        executorWithLinkedQueue.shutdown();
        executorWithSyncQueue.shutdown();
        executorWithPriorityQueue.shutdown();
    }
}

在上述示例中,展示了如何使用不同类型的任务队列来创建线程池。在实际应用中,应根据任务的特点和系统的需求选择合适的任务队列。例如,如果任务量相对稳定且希望对队列容量进行控制,可以选择 ArrayBlockingQueue;如果任务处理速度较快,不希望任务在队列中积压,可以选择 SynchronousQueue;如果对任务执行顺序有要求,则可以选择 PriorityBlockingQueue

6. threadFactory(线程工厂)

  • 含义:用于创建新线程的工厂。通过自定义线程工厂,可以对新创建的线程进行一些个性化设置,如线程名称、线程优先级、是否为守护线程等。
  • 作用:线程工厂提供了一种统一的方式来创建线程,使得线程的创建过程更加灵活和可控。它有助于提高代码的可维护性和可读性,特别是在需要对线程进行特定配置的场景下。例如,在大型项目中,通过自定义线程工厂,可以为不同模块的线程设置不同的命名规则,方便在日志和调试过程中区分和追踪线程。

代码示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class ThreadFactoryExample {
    public static void main(String[] args) {
        ThreadFactory threadFactory = new ThreadFactory() {
            private int counter = 1;

            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("CustomThread-" + counter++);
                thread.setPriority(Thread.NORM_PRIORITY);
                return thread;
            }
        };

        // 使用自定义线程工厂创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2, threadFactory);

        for (int i = 0; i < 5; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskNumber + " is being processed by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executorService.shutdown();
    }
}

在上述代码中,定义了一个自定义线程工厂 threadFactory,它为新创建的线程设置了自定义的名称和默认优先级。然后使用这个线程工厂创建了一个固定线程数为 2 的线程池。当提交任务时,打印出的线程名称就会是自定义的格式,方便识别和管理。

7. handler(拒绝策略)

  • 含义:当线程池无法继续接受任务时(队列已满且线程数达到 maximumPoolSize),所采取的拒绝策略。Java 提供了几种内置的拒绝策略,也允许用户自定义拒绝策略。
  • 作用:拒绝策略保证了在极端情况下,线程池不会因为无法处理任务而导致系统崩溃。通过合理选择拒绝策略,可以优雅地处理任务无法执行的情况,如记录日志、丢弃任务、抛出异常等,以满足不同应用场景的需求。常见的拒绝策略有:
    • AbortPolicy:默认的拒绝策略,当任务无法处理时,直接抛出 RejectedExecutionException 异常,阻止系统正常运行。这种策略适用于需要立即知道任务是否被成功处理的场景,如果任务不能被处理,系统可能需要做出相应的处理措施。
    • CallerRunsPolicy:将任务返回给调用者,由调用者所在的线程来执行任务。这种策略可以降低新任务的提交速度,因为调用者线程在执行任务时,无法同时提交新任务,从而减轻线程池的压力。适用于对响应时间要求不高,但希望能尽可能处理任务的场景。
    • DiscardPolicy:直接丢弃无法处理的任务,不做任何提示。这种策略适用于那些对任务丢失不太敏感的场景,例如一些非关键的日志记录任务等。
    • DiscardOldestPolicy:丢弃队列中最老的一个任务(即最早进入队列的任务),然后尝试重新提交当前任务。这种策略适用于希望优先处理新任务,并且对任务顺序不太敏感的场景。

代码示例

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.RejectedExecutionHandler;

public class RejectedExecutionHandlerExample {
    public static void main(String[] args) {
        // 创建一个线程池,核心线程数为 2,最大线程数为 4,队列容量为 3
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                2,
                4,
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println("Task " + r + " is rejected. Thread pool is full.");
                    }
                }
        );

        for (int i = 0; i < 10; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskNumber + " is being processed by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executorService.shutdown();
    }
}

在上述示例中,自定义了一个拒绝策略,当任务被拒绝时,打印出相应的提示信息。在实际应用中,可以根据业务需求选择合适的内置拒绝策略或自定义拒绝策略。例如,如果任务非常重要,不允许丢失,可以选择 AbortPolicy,通过捕获异常来进行相应的处理;如果希望尽可能处理任务,并且对响应时间要求不高,可以选择 CallerRunsPolicy

深入理解 Java 线程池的这些核心参数,能够帮助我们根据不同的业务场景,合理地配置线程池,从而充分发挥线程池的优势,提高应用程序的性能和稳定性。在实际开发中,应根据任务的特性、系统资源状况等因素,仔细权衡和调整这些参数,以达到最优的线程池使用效果。