1. 写在前面
Java 中的线程池是一种管理和复用线程的机制,用于减少线程创建和销毁的开销,提高应用程序的性能和资源利用率。线程池通过预先创建一定数量的线程,并在需要时复用这些线程来执行任务,从而避免了频繁的线程创建和销毁。在正式阅读线程池相关的代码之前,我先抛出几个问题看看大家有没有思考过:
- Java 中如何创建线程池?
- 线程池的核心参数有哪些?
- ThreadPoolExecutor 的工作原理是什么?
- 什么是拒绝策略?Java 提供了哪些拒绝策略?
- 如何优雅地关闭线程池?
- 如何创建一个自定义线程池?
- 如何监控线程池的状态?
- 在什么场景下使用 FixedThreadPool?
- 在什么场景下使用 CachedThreadPool?
- ScheduledThreadPoolExecutor 的用途是什么?
- 如何处理线程池中的异常?
2. 从使用说起
以下是一个简单的示例,展示了如何使用固定大小的线程池来执行多个任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(() -> {
System.out.println("Task " + index + " is running in thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池
fixedThreadPool.shutdown();
}
}
2.1 线程池的基本概念
- 线程池:一个包含多个线程的集合,这些线程可以被重复使用来执行多个任务。
- 任务:需要在线程中执行的工作单元,通常是实现了 Runnable 接口或 Callable 接口的对象。
- 工作队列:用于保存等待执行的任务的队列。
- 线程池管理器:负责管理线程的创建、调度和销毁。
2.2 Java 中的线程池实现
Java 提供了 java.util.concurrent 包中的 Executor 框架来实现线程池。主要的类和接口包括:
- Executor:一个简单的任务执行器接口,定义了 execute(Runnable command) 方法。
- ExecutorService:继承自 Executor,提供了更丰富的线程池管理功能,如提交任务、关闭线程池等。
- ThreadPoolExecutor:ExecutorService 的一个实现类,提供了灵活的线程池配置选项。
- Executors:一个工厂类,提供了多种创建线程池的方法。
2.3 常见的线程池类型
通过 Executors 工厂类可以创建不同类型的线程池:
- FixedThreadPool:固定大小的线程池,适用于负载较重的服务器。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
- CachedThreadPool:可缓存的线程池,适用于执行大量短期异步任务。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- SingleThreadExecutor:单线程的线程池,适用于需要顺序执行任务的场景。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
- ScheduledThreadPool:支持定时和周期性任务执行的线程池。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
2.4 线程池的主要方法
- submit:提交一个任务,返回一个 Future 对象,可以用来获取任务的执行结果或取消任务。
Future<Integer> future = fixedThreadPool.submit(() -> {
// 任务逻辑
return 42;
});
- execute:提交一个 Runnable 任务,任务没有返回值。
fixedThreadPool.execute(() -> {
// 任务逻辑
});
- shutdown:关闭线程池,已提交的任务会继续执行,但不再接受新任务。
fixedThreadPool.shutdown();
- shutdownNow:立即关闭线程池,尝试停止所有正在执行的任务并返回等待执行的任务列表。
List<Runnable> notExecutedTasks = fixedThreadPool.shutdownNow();
- awaitTermination:等待所有任务完成或超时。
fixedThreadPool.awaitTermination(1, TimeUnit.MINUTES);
2.5 线程池的优势
- 减少线程创建和销毁的开销:通过复用线程,避免了频繁的线程创建和销毁,降低了系统资源的消耗。
- 提高响应速度:任务到达时不需要等待线程创建,可以立即执行,提高了响应速度。
- 更好的资源管理:通过限制线程池中的线程数量,可以避免资源的过度使用,防止系统过载。
- 统一的任务管理:线程池提供了统一的任务提交、执行、管理和监控机制,简化了多线程编程。
2.6 线程池的配置参数
通过 ThreadPoolExecutor 可以灵活配置线程池的参数:
- corePoolSize:核心线程数,线程池中始终保留的线程数。
- maximumPoolSize:最大线程数,线程池中允许的最大线程数。
- keepAliveTime:线程空闲时间,超过这个时间的空闲线程会被销毁。
- unit:时间单位,配合 keepAliveTime 使用。
- workQueue:任务队列,保存等待执行的任务。
- threadFactory:线程工厂,用于创建新线程。
- handler:拒绝策略,当任务太多无法处理时的处理方式。
下面的代码示例展示了这些参数的使用:
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 自定义线程池
ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
2, // corePoolSize
4, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingQueue<>(10), // workQueue
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.AbortPolicy() // handler
);
for (int i = 0; i < 15; i++) {
final int index = i;
customThreadPool.execute(() -> {
System.out.println("Task " + index + " is running in thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池
customThreadPool.shutdown();
}
}
3. ThreadPoolExecutor 的工作原理是什么?
- 当提交任务时,如果当前线程数小于 corePoolSize,则创建新线程执行任务。
- 如果当前线程数达到 corePoolSize,任务被加入到 workQueue 中。
- 如果 workQueue 满了,且线程数小于 maximumPoolSize,则创建新线程执行任务。
- 如果线程数达到 maximumPoolSize,则根据拒绝策略处理任务。
4. 什么是拒绝策略?Java 提供了哪些拒绝策略?
拒绝策略是在线程池无法处理新任务时的处理方式。Java 提供了以下几种拒绝策略:
- AbortPolicy:抛出 RejectedExecutionException。
- CallerRunsPolicy:由调用线程执行任务。
- DiscardPolicy:丢弃任务,不抛出异常。
- DiscardOldestPolicy:丢弃最早的未处理任务,然后尝试重新提交任务。
这里我再问一个高级面试题:线程池的拒绝策略是谁来执行的? 我们在文末给出答案,大家想思考下。
5. 如何监控线程池的状态?
可以通过 ThreadPoolExecutor 提供的方法获取线程池的状态信息:
- getPoolSize:获取当前线程池中的线程数。
- getActiveCount:获取当前正在执行任务的线程数。
- getCompletedTaskCount:获取已完成的任务数。
- getTaskCount:获取已提交的任务总数。
6. ScheduledThreadPoolExecutor 的用途是什么?
ScheduledThreadPoolExecutor 是 Java 中用于执行定时任务和周期性任务的线程池实现。它继承自 ThreadPoolExecutor,并扩展了其功能,使其能够调度任务在指定的时间点执行或以固定的时间间隔重复执行。
6.1 主要用途
- 定时任务:在指定的延迟时间后执行一次任务。
- 周期性任务:以固定的时间间隔重复执行任务。
6.2 主要方法
- schedule(Runnable command, long delay, TimeUnit unit):在指定的延迟时间后执行一次任务。
- schedule(Callable callable, long delay, TimeUnit unit):在指定的延迟时间后执行一次任务,并返回一个 Future 代表任务的结果。
- scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):以固定的时间间隔重复执行任务,初始延迟后第一次执行,然后每隔指定时间执行一次。
- scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):以固定的延迟时间重复执行任务,初始延迟后第一次执行,然后每次任务执行完毕后等待指定的时间再执行下一次。
6.3 定时任务
在指定的延迟时间后执行一次任务:
import java.util.concurrent.*;
public class ScheduledThreadPoolExecutorExample {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Task executed at " + System.currentTimeMillis());
// 在3秒后执行一次任务
scheduledExecutorService.schedule(task, 3, TimeUnit.SECONDS);
// 关闭线程池
scheduledExecutorService.shutdown();
}
}
6.4 周期性任务(固定速率)
以固定的时间间隔重复执行任务:
import java.util.concurrent.*;
public class ScheduledThreadPoolExecutorFixedRateExample {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Task executed at " + System.currentTimeMillis());
// 在初始延迟1秒后,以固定的时间间隔3秒重复执行任务
scheduledExecutorService.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);
// 运行一段时间后关闭线程池
try {
Thread.sleep(10000); // 运行10秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
scheduledExecutorService.shutdown();
}
}
6.5 周期性任务(固定延迟)
每次任务执行完毕后等待指定的时间再执行下一次:
import java.util.concurrent.*;
public class ScheduledThreadPoolExecutorFixedDelayExample {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
System.out.println("Task executed at " + System.currentTimeMillis());
try {
Thread.sleep(2000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
// 在初始延迟1秒后,每次任务执行完毕后等待3秒再执行下一次
scheduledExecutorService.scheduleWithFixedDelay(task, 1, 3, TimeUnit.SECONDS);
// 运行一段时间后关闭线程池
try {
Thread.sleep(10000); // 运行10秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
scheduledExecutorService.shutdown();
}
}
7. 如何处理线程池中的异常?
在使用线程池时,处理线程中的异常是非常重要的。因为线程池中的任务是异步执行的,如果任务抛出异常而不进行处理,可能会导致任务失败且无法被检测到,从而影响系统的稳定性和可靠性。
以下是几种处理线程池中异常的方法:
7.1 使用 Future 获取异常
当提交任务时,可以使用 submit 方法返回一个 Future 对象,通过 Future.get() 方法可以捕获任务执行过程中抛出的异常。
import java.util.concurrent.*;
public class FutureExceptionHandlingExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Callable<Integer> task = () -> {
System.out.println("Task started");
throw new RuntimeException("Exception in task");
};
Future<Integer> future = executorService.submit(task);
try {
future.get(); // This will throw ExecutionException if the task threw an exception
} catch (InterruptedException | ExecutionException e) {
System.err.println("Exception caught: " + e.getCause());
}
executorService.shutdown();
}
}
7.2 重写 ThreadPoolExecutor 的 afterExecute 方法
通过重写 ThreadPoolExecutor 的 afterExecute 方法,可以在任务执行完毕后捕获异常。
import java.util.concurrent.*;
public class AfterExecuteExceptionHandlingExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
Object result = ((Future<?>) r).get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null) {
System.err.println("Exception caught: " + t);
}
}
};
Runnable task = () -> {
System.out.println("Task started");
throw new RuntimeException("Exception in task");
};
executor.execute(task);
executor.shutdown();
}
}
7.3 使用自定义的 ThreadFactory
通过自定义 ThreadFactory,可以为每个线程设置一个 UncaughtExceptionHandler,捕获线程中未捕获的异常。
import java.util.concurrent.*;
public class ThreadFactoryExceptionHandlingExample {
public static void main(String[] args) {
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler((t, e) -> {
System.err.println("Exception caught in thread " + t.getName() + ": " + e);
});
return thread;
}
};
ExecutorService executorService = Executors.newFixedThreadPool(2, threadFactory);
Runnable task = () -> {
System.out.println("Task started");
throw new RuntimeException("Exception in task");
};
executorService.execute(task);
executorService.shutdown();
}
}
7.4 使用 try-catch 块
在任务内部使用 try-catch 块捕获异常,并进行相应的处理。
import java.util.concurrent.*;
public class TryCatchExceptionHandlingExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Runnable task = () -> {
try {
System.out.println("Task started");
throw new RuntimeException("Exception in task");
} catch (Exception e) {
System.err.println("Exception caught in task: " + e);
}
};
executorService.execute(task);
executorService.shutdown();
}
}
8. 线程池的拒绝策略是谁执行的?
拒绝策略是由调用线程池的 execute 方法的线程执行的。也就是说,当你在主线程或其他线程中调用线程池的 execute 方法时,如果任务被拒绝,拒绝策略会在调用 execute 方法的那个线程中执行。
系列文章
7.jdk源码阅读之ConcurrentHashMap(上)