线程池与Executor框架

177 阅读6分钟

1. 线程池的概念

线程池是一种管理线程的机制,通过复用线程池中的线程来执行多个任务,减少创建和销毁线程所带来的性能开销。线程池在应用程序启动时创建多个线程,任务提交到线程池后,线程池中的线程会自动执行这些任务。当任务完成后,线程不会被销毁,而是继续等待下一个任务。

2. 为什么要使用线程池?

线程池具有以下优点:

  • 降低资源消耗:通过重复利用线程,避免了频繁创建和销毁线程所带来的性能开销。
  • 提高响应速度:线程池中的线程已创建并准备好执行任务,从而缩短了任务启动时间。
  • 提高线程的可管理性:线程池可以限制线程的数量,避免线程数量过多导致的资源耗尽问题。
  • 提供更强大的功能:线程池提供了定时执行、定期执行等高级功能。

3. Java中的线程池实现

Java通过java.util.concurrent包中的Executor接口和ThreadPoolExecutor类实现了线程池。Executor接口定义了一个执行任务的方法,而ThreadPoolExecutorExecutor的一个具体实现,提供了丰富的线程池配置选项。

4. Executor框架

Executor框架是Java提供的一套线程池解决方案,提供了简单易用的API来执行异步任务。Executor框架的核心接口是ExecutorExecutorService,分别定义了线程池的基本操作和生命周期管理。Executors类提供了创建和配置线程池的工厂方法。

5.Executor框架的使用

使用Executor框架创建线程池的基本步骤如下:

  • 创建线程池:通过Executors类的工厂方法创建线程池,如newFixedThreadPool、newCachedThreadPool、newScheduledThreadPool等。
  • 提交任务:使用ExecutorService的submit()或execute()方法提交任务。
  • 关闭线程池:使用ExecutorService的shutdown()方法关闭线程池。

示例:

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

public class ExecutorExample {

    public static void main(String[] args) {
        // 创建一个拥有5个线程的FixedThreadPool
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

        // 提交10个任务到线程池
        for (int i = 0; i < 10; i++) {
            final int taskId = i + 1;
            fixedThreadPool.submit(() -> {
                System.out.println("Task " + taskId + " is being executed by thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        fixedThreadPool.shutdown();
    }
}

6. 常用的线程池类型

Java提供了几种预定义的线程池,可以通过Executors类的工厂方法创建:

  1. FixedThreadPool:固定数量的线程池。
  2. CachedThreadPool:缓存线程池,根据需要创建新线程,空闲线程会被回收。
  3. SingleThreadExecutor:单线程的线程池,所有任务按顺序执行。
  4. ScheduledThreadPool:支持定时和周期性任务的线程池。

  1. FixedThreadPool:固定大小线程池 FixedThreadPool是一个拥有固定线程数量的线程池。当任务到来时,若线程池中有空闲线程,则立即执行;若没有空闲线程,则任务进入等待队列,等待其他任务完成后执行。FixedThreadPool适用于处理需要并发执行的任务数量有限的场景。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
  1. CachedThreadPool:缓存线程池

CachedThreadPool是一个可以缓存线程的线程池。当任务到来时,若线程池中有空闲线程,则立即执行;若没有空闲线程且线程池未达到最大容量,则创建新线程执行。CachedThreadPool适用于执行大量短时任务的场景。

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  1. ScheduledThreadPool:定时线程池

ScheduledThreadPool是一个可以执行定时任务和周期性任务的线程池。它提供了类似于Timer的功能,但拥有更高的性能。ScheduledThreadPool适用于需要执行定时任务和周期性任务的场景。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
  1. SingleThreadExecutor:单线程池

SingleThreadExecutor是一个只有一个线程的线程池。它可以保证提交的任务按顺序执行,适用于需要顺序执行任务的场景。

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

7. 线程池的重要参数:

  1. 核心线程数(corePoolSize):线程池中始终保持的线程数量,即使这些线程处于空闲状态。
  2. 最大线程数(maximumPoolSize):线程池允许的最大线程数量。
  3. 线程空闲时间(keepAliveTime):当线程池中线程数量超过核心线程数时,空闲线程在被回收前可以保持的最长时间。
  4. 时间单位(timeUnit):线程空闲时间的单位。
  5. 等待队列(workQueue):存储等待执行的任务的队列。
  6. 线程工厂(threadFactory):用于创建线程的工厂,可以自定义线程的属性,如名称、优先级等。
  7. 拒绝策略(rejectedExecutionHandler):当线程池无法处理新任务时所采取的措施。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomThreadPoolExample {

    public static void main(String[] args) {
        ThreadFactory customThreadFactory = new ThreadFactory() {
            private final AtomicInteger threadNumber = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "CustomThread-" + threadNumber.getAndIncrement());
                t.setPriority(Thread.NORM_PRIORITY);
                return t;
            }
        };

        ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60, // 线程空闲时间
                TimeUnit.SECONDS, // 时间单位
                new ArrayBlockingQueue<>(10), // 等待队列
                customThreadFactory, // 自定义线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 提交任务到自定义线程池
        for (int i = 0; i < 20; i++) {
            final int taskId = i;

            customThreadPool.execute(() -> {
                System.out.println("Task " + taskId + " is executed by thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 模拟任务执行时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        customThreadPool.shutdown();
    }
}


8. 线程池的拒绝策略 Java提供了四种预定义的拒绝策略:

线程池的拒绝策略(RejectedExecutionHandler)是当线程池无法处理新提交的任务时所采取的措施。线程池可能因为达到最大线程数、等待队列已满等原因而无法处理新任务。

  1. AbortPolicy:抛出RejectedExecutionException异常。
  2. CallerRunsPolicy:提交任务的线程自己执行任务。
  3. DiscardPolicy:直接丢弃任务。
  4. DiscardOldestPolicy:丢弃等待队列中最旧的任务,然后尝试重新提交当前任务。 你还可以实现RejectedExecutionHandler接口,自定义拒绝策略。

  1. AbortPolicy(默认策略)

AbortPolicy策略会抛出一个未检查的RejectedExecutionException异常,表示任务无法被执行。这种策略直接告知调用者任务无法执行,但可能导致调用者程序终止。

RejectedExecutionHandler abortPolicy = new ThreadPoolExecutor.AbortPolicy();
  1. CallerRunsPolicy

CallerRunsPolicy策略使得提交任务的线程自己去执行该任务。这种策略降低了任务在新线程中执行的并发性,但可以有效降低任务提交速度,从而使得线程池有更多空闲时间处理等待队列中的任务。

RejectedExecutionHandler callerRunsPolicy = new ThreadPoolExecutor.CallerRunsPolicy();
  1. DiscardPolicy

DiscardPolicy策略直接丢弃无法执行的任务,不会给调用者任何反馈。这种策略适用于可以容忍任务丢失的场景。

RejectedExecutionHandler discardPolicy = new ThreadPoolExecutor.DiscardPolicy();
  1. DiscardOldestPolicy

DiscardOldestPolicy策略丢弃等待队列中最旧的任务,然后尝试重新提交当前任务。这种策略适用于希望优先执行新任务的场景。

RejectedExecutionHandler discardOldestPolicy = new ThreadPoolExecutor.DiscardOldestPolicy();

除了使用预定义的拒绝策略外,你还可以实现RejectedExecutionHandler接口,自定义拒绝策略。以下是一个简单的自定义拒绝策略示例:

class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("Custom rejection policy: Task " + r.toString() + " is rejected.");
    }
}

在创建ThreadPoolExecutor时,可以通过设置RejectedExecutionHandler参数来指定拒绝策略:

ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
        2, // 核心线程数
        4, // 最大线程数
        60, // 线程空闲时间
        TimeUnit.SECONDS, // 时间单位
        new ArrayBlockingQueue<>(10), // 等待队列
        new CustomRejectedExecutionHandler() // 自定义拒绝策略
);

面试题

1. 什么是线程池?为什么需要使用线程池?

线程池是一种管理线程的机制,它维护一组线程,当需要执行任务时,会从线程池中分配一个空闲线程来执行任务。线程池可以有效控制线程数量,避免线程过多导致的资源消耗和系统崩溃。使用线程池可以减少线程创建和销毁的开销,提高系统性能。

2. 请简述Java中的Executor框架。

Executor框架是Java提供的一套线程池管理和任务调度的API,它位于java.util.concurrent包中。主要包括Executor接口、ThreadPoolExecutor类、Executors工具类以及一些线程池相关的辅助类。Executor框架使得线程池的创建和管理更加简单和灵活。

3. Java中有哪些常用的线程池?

Java提供了以下四种常用的线程池:

  • FixedThreadPool:固定大小的线程池,所有线程都是核心线程,不会被回收。
  • CachedThreadPool:可缓存的线程池,线程数量不固定,空闲线程可被回收。
  • ScheduledThreadPool:定时任务线程池,用于执行定时或周期性任务。
  • SingleThreadExecutor:单线程线程池,只有一个线程,任务按顺序执行。

4. 请列举创建线程池时的一些重要参数。

创建线程池时的一些重要参数包括:

  • 核心线程数(corePoolSize):线程池中始终保持的线程数量。
  • 最大线程数(maximumPoolSize):线程池允许的最大线程数量。
  • 线程空闲时间(keepAliveTime):空闲线程在被回收前可以保持的最长时间。
  • 时间单位(timeUnit):线程空闲时间的单位。
  • 等待队列(workQueue):存储等待执行的任务的队列。
  • 线程工厂(threadFactory):用于创建线程的工厂,可以自定义线程的属性,如名称、优先级等。
  • 拒绝策略(rejectedExecutionHandler):当线程池无法处理新任务时所采取的措施。

5. 线程池中有哪些拒绝策略?

Java提供了四种预定义的拒绝策略:

  • AbortPolicy:抛出RejectedExecutionException异常。
  • CallerRunsPolicy:提交任务的线程自己执行任务。
  • DiscardPolicy:直接丢弃任务。
  • DiscardOldestPolicy:丢弃等待队列中最旧的任务,然后尝试重新提交当前任务。

6. 如何在Java中优雅地关闭线程池?

在Java中,可以使用shutdown()shutdownNow()方法来关闭线程池。shutdown()方法会等待正在执行的任务完成和等待队列中的任务执行完毕后,才会关闭线程池。而shutdownNow()方法会尝试立即停止正在执行的任务,并返回等待队列中尚未开始执行的任务列表。通常,我们使用shutdown()方法来优雅地关闭线程池。

7. 为什么要避免使用Executors类创建的默认线程池?

虽然Executors类提供了创建线程池的便捷方法,但它们的配置可能导致系统资源耗尽。例如,newCachedThreadPool创建的线程池允许无限制地创建新线程,这可能导致过多的线程消耗系统资源,进而影响系统性能。newFixedThreadPoolnewSingleThreadExecutor创建的线程池使用无界的等待队列,这可能导致队列无限制地增长,消耗内存资源。因此,通常建议创建自定义线程池,以便更好地控制线程池的各项参数。

8. 在Java中如何处理线程池中的异常?

要处理线程池中的异常,可以使用Future来接收线程池执行任务的结果。当调用Future.get()方法时,如果任务执行过程中抛出了异常,那么get()方法将抛出ExecutionException异常。可以通过捕获该异常并调用getCause()方法来获取原始异常信息。

另一种方法是实现Thread.UncaughtExceptionHandler接口,为线程设置一个未捕获异常处理器。在自定义的线程工厂中创建线程时,为线程设置该处理器。这样,当线程执行过程中抛出异常时,未捕获异常处理器的uncaughtException()方法将被调用。

9. 如何实现一个延迟任务或定时任务?

要实现延迟任务或定时任务,可以使用ScheduledThreadPoolExecutor类。它是ThreadPoolExecutor的一个子类,提供了任务调度功能。

可以使用Executors.newScheduledThreadPool方法创建一个定时任务线程池。然后,可以使用schedule方法来安排一个延迟任务,或使用scheduleAtFixedRatescheduleWithFixedDelay方法来安排定时任务。例如:

// 创建一个具有2个线程的定时任务线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);

// 提交一个延迟3秒执行的任务
scheduledExecutorService.schedule(new RunnableTask(), 3, TimeUnit.SECONDS);

// 提交一个初始延迟1秒,每隔2秒执行一次的任务
scheduledExecutorService.scheduleAtFixedRate(new RunnableTask(), 1, 2, TimeUnit.SECONDS);

// 提交一个初始延迟1秒,每次任务执行完毕后等待2秒再执行下一次任务的任务
scheduledExecutorService.scheduleWithFixedDelay(new RunnableTask(), 1, 2, TimeUnit.SECONDS);

10. 什么是Java中的CompletableFuture?

CompletableFuture是Java 8引入的一种异步编程工具,位于java.util.concurrent包中。它实现了FutureCompletionStage接口,可以用于表示异步计算的结果。与Future相比,CompletableFuture提供了更多的功能,如组合多个异步任务、异常处理、任务完成时的回调等。

要使用CompletableFuture,可以使用supplyAsyncrunAsync等方法创建一个新的CompletableFuture实例,并将要执行的任务传入。然后,可以使用thenApplythenAcceptthenRun等方法来定义任务完成时的回调,或使用exceptionally方法来处理异常。例如:

CompletableFuture.supplyAsync(() -> {
    // 异步执行的任务
    return "Hello, world!";
}).thenAccept(result -> {
    // 任务完成时的回调
    System.out.println(result); 
    }).exceptionally(ex -> { // 异常处理 System.out.println("Error: " + ex.getMessage()); return null; });

CompletableFuture还提供了一些静态方法,如allOfanyOf,用于组合多个CompletableFuture`实例。例如:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello"); 
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World"); 
// 等待所有任务完成
CompletableFuture<Void> allFutures =CompletableFuture.allOf(future1, future2); 
// 等待任何一个任务完成
CompletableFuture<Object> anyFutures = CompletableFuture.anyOf(future1, future2); 
// 获取任务结果
String result1 = future1.join(); 
String result2 = future2.join();

通过CompletableFuture,我们可以更方便地实现异步任务的编排和组合,提高程序的响应性和性能。