Java 线程池是一个提供多线程管理和调度的工具,通常用来处理多个并发任务。线程池能够帮助有效管理线程的创建、调度、执行和销毁,避免频繁的线程创建和销毁,提高系统性能。
前言
Java 线程池是面试中的常客,面试官经常会问线程池的作用和线程池的创建销毁这些基础问题,本文就Java线程池的基本概念、工作原理、实际案例展开阐述。
一、基本概念
线程池本质上是一个管理线程的容器,它包含了多个线程,可以用来执行多个任务。线程池的核心思想是:复用已创建的线程,避免频繁的线程创建和销毁操作。
为什么要使用线程池:
- 性能稳定性强:通过复用线程,避免频繁创建和销毁线程带来的开销。
- 提高资源管理效率:线程池管理线程的最大数量,防止线程过多而导致资源竞争和系统崩溃。
- 配置灵活:线程池的参数可以根据实际需求调整,允许在不同的任务量和工作负载下调整线程池的大小。
- 支持定时以及周期性任务执行:线程池可以方便地支持定时任务和周期性任务的执行,这对于需要定时执行任务的应用非常有用。
二、核心接口
在Java中,线程池的核心接口和类主要位于java.util.concurrent
包中。线程池结构图如下:
1. Executor 接口
Executor
是线程池的最基本接口,它负责提交任务。定义了执行任务的基本方法。
public interface Executor {
void execute(Runnable command); // 提交任务给线程池
}
2. ExecutorService 接口
ExecutorService
是 Executor 的子接口,增加了用于管理线程池的方法。常用的方法包括:
submit()
提交任务并返回一个 Future 对象,允许获取任务执行结果或取消任务。invokeAll()
提交多个任务并返回一个 List,用于批量处理任务。shutdown()
和shutdownNow()
用于关闭线程池。
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task); // 提交带返回值的任务
<T> Future<T> submit(Runnable task, T result); // 提交Runnable任务,并返回结果
List<Runnable> shutdownNow(); // 强制关闭线程池
void shutdown(); // 优雅地关闭线程池
}
3. ThreadPoolExecutor 类
ThreadPoolExecutor
是 ExecutorService 的核心实现类,提供了灵活配置线程池参数的方法。常用的构造函数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
- corePoolSize:核心线程数,线程池最小线程数。
- maximumPoolSize:最大线程数,线程池最多允许的线程数。
- keepAliveTime:当线程池的线程超过 corePoolSize 时,空闲线程的最大存活时间。
- unit:keepAliveTime 的时间单位。
- workQueue:任务队列,用于存储等待执行的任务。
常用线程池类:
newFixedThreadPool(int nThreads)
:创建固定大小的线程池,nThreads 为线程数。newCachedThreadPool()
:创建可缓存的线程池,能够根据需要创建线程,线程空闲时会被回收。newSingleThreadExecutor()
:创建单线程池,始终只有一个工作线程。newScheduledThreadPool(int corePoolSize)
:创建定时任务线程池,支持定时和周期性任务。
三、线程池创建关闭
1、使用 ExecutorService 创建和使用线程池
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个线程池,最大线程数为 10,核心线程数为 5,任务队列长度为 100
ExecutorService executorService = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
);
// 提交 10 个任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
executorService.submit(() -> {
System.out.println("Executing task " + taskId + " by " + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
2、直接使用ThreadPoolExecutor类创建线程池
可以直接使用ThreadPoolExecutor类构造器来创建线程池,这样可以更灵活地设置参数,如核心线程数、最大线程数、工作队列、线程工厂、拒绝策略等。
int corePoolSize = 10;
int maximumPoolSize = 50;
long keepAliveTime = 120;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ExecutorService threadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
3、关闭线程池
shutdown()
:线程池会等到已经提交的任务执行完成后关闭,无法接受新的任务。shutdownNow()
:线程池尝试停止所有正在执行的任务,并返回待执行任务的列表。
四、线程池工作原理
当一个任务被提交到线程池时,线程池根据其当前的状态决定如何处理任务:
- 如果当前工作线程数小于
corePoolSize
,线程池会创建一个新的线程来处理任务。 - 如果当前工作线程数等于或大于
corePoolSize
,且队列尚未满,线程池将任务添加到队列中。 - 如果当前工作线程数大于
corePoolSize
且队列已满,线程池会创建新的线程,直到达到maximumPoolSize
。 - 如果所有线程都在工作且队列已满,新的任务将被拒绝,具体的拒绝策略取决于
RejectedExecutionHandler
的实现(默认是抛出异常)。
线程池任务处理流程图如下:
五、线程池异常处理
1. ThreadPoolExecutor 的异常处理机制
在使用 ThreadPoolExecutor
提交任务时,任务执行过程中的异常处理取决于几个因素,尤其是任务的类型(Runnable
或 Callable
)以及异常的传播方式。
Runnable
接口中的异常
Runnable 是不返回结果的任务接口,它的run()
方法不能抛出任何异常。因此,如果在run()
方法中发生了异常,默认情况下,异常会被吞掉,线程池不会进行任何处理。
默认行为:
线程池不会捕获Runnable
中的异常。异常会在执行该任务的线程中被丢弃,不会传播到线程池外部。任务失败的信息不会返回给调用者。如果想要捕获并处理Runnable
中的异常,你需要在run()
方法内部手动捕获异常,并进行相应的处理。
Runnable task = () -> {
try {
// 任务代码
throw new RuntimeException("Error in task");
} catch (Exception e) {
System.out.println("Exception caught: " + e.getMessage());
// 处理异常,如记录日志
}
};
executor.execute(task);
Callable
接口中的异常
Callable 是带有返回值的任务接口,它的call()
方法允许抛出异常。如果任务中的call()
方法抛出异常,线程池会将异常封装到Future
对象中,调用者可以通过Future.get()
方法获取异常。
处理 Callable 中的异常:
如果call()
方法抛出异常,Future.get()
会抛出 ExecutionException,并且原始异常会作为 ExecutionException 的cause
。
可以通过Future.get()
捕获并处理异常。
Callable<String> task = () -> {
if (true) {
throw new RuntimeException("Error in callable task");
}
return "Task Completed";
};
ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<String> future = executorService.submit(task);
try {
// 获取结果,如果有异常会抛出 ExecutionException
String result = future.get();
System.out.println(result);
} catch (ExecutionException e) {
System.out.println("Task failed with exception: " + e.getCause());
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
}
2. 线程池的异常处理机制:RejectedExecutionHandler
当线程池的工作队列已满,或线程池的线程数已经达到最大值时,任务会被拒绝执行。这时,线程池使用 RejectedExecutionHandler 来处理拒绝的任务。
RejectedExecutionHandler
是一个接口,它有四个常用实现:
- AbortPolicy:默认策略,抛出
RejectedExecutionException
。 - CallerRunsPolicy:由提交任务的线程来执行该任务。
- DiscardPolicy:丢弃被拒绝的任务。
- DiscardOldestPolicy:丢弃队列中最旧的任务,并执行当前任务。
如果线程池中的任务被拒绝执行,RejectedExecutionHandler
会被调用,允许我们自定义如何处理这些被拒绝的任务。可以在此处捕获和处理任务拒绝相关的异常。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardOldestPolicy() // 自定义的拒绝策略
);
Runnable task = () -> {
System.out.println("Executing task");
};
for (int i = 0; i < 5; i++) {
executor.execute(task); // 会抛出被拒绝的任务
}
3. 自定义异常处理机制
可以通过 ThreadFactory
来为线程池中的每个线程提供自定义的异常处理机制。在 ThreadFactory
创建线程时,可以设置线程的 uncaughtExceptionHandler
,这样可以捕获线程执行时未处理的异常。
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("Thread " + t.getName() + " failed with exception: " + e.getMessage());
});
return thread;
}
};
ExecutorService executorService = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1),
threadFactory
);
executorService.submit(() -> {
throw new RuntimeException("Exception in thread");
});
六、线程池应用场景
1、固定大小线程池
固定线程池(FixedThreadPool)是线程池的一种实现方式,它通过固定数量的线程来执行提交的任务。在这种线程池中,线程的数量是固定的,线程池的大小在创建时被设定好,且不会发生变化。即使有大量的任务提交,线程池也只会使用有限数量的线程去处理任务,超出线程池容量的任务将被放入等待队列,直到线程池中的某个线程完成任务并空闲出来,才能继续执行新的任务。适用于任务量比较稳定的场景,可以高效管理线程资源。示意图如下:
使用 FixedThreadPool
创建线程池
import java.util.concurrent.*;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池,线程数为 3
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交 5 个任务
for (int i = 0; i < 5; i++) {
final int taskId = i;
executorService.submit(() -> {
try {
System.out.println("Task " + taskId + " is being executed by " + Thread.currentThread().getName());
// 模拟任务执行
Thread.sleep(1000);
System.out.println("Task " + taskId + " completed by " + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
2、缓存型线程池
CachedThreadPool
是一种能够根据需求动态创建线程的线程池实现,其特点是当任务较少时,线程池的大小可以非常小,甚至为 0;当任务增多时,线程池可以根据需要动态创建新的线程来处理任务。线程池中的线程在任务完成后不会立即销毁,而是会被缓存一段时间。如果有新的任务提交,线程池会复用这些空闲的线程;如果任务长时间没有提交,空闲的线程会被销毁。适用于任务量不稳定的场景,可以根据任务需求动态增加线程数,线程空闲时会被回收。示意图如下:
使用 CachedThreadPool
创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
3、定时任务调度线程池
调度线程池可以设定一个周期,按照这个周期重复执行任务。适用于需要定时或周期性执行任务的场景。示意图如下:
创建 ScheduledThreadPool
import java.util.concurrent.*;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
// 创建一个大小为 3 的调度线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
// 1. 延迟 2 秒执行一次任务
scheduledExecutorService.schedule(() -> {
System.out.println("Task with delay executed at " + System.currentTimeMillis());
}, 2, TimeUnit.SECONDS);
// 2. 每 3 秒执行一次任务
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("Periodic task executed at " + System.currentTimeMillis());
}, 1, 3, TimeUnit.SECONDS);
// 3. 每 3 秒执行一次任务,首次任务会在延迟 1 秒后执行
scheduledExecutorService.scheduleWithFixedDelay(() -> {
System.out.println("Periodic task with fixed delay executed at " + System.currentTimeMillis());
}, 1, 3, TimeUnit.SECONDS);
// 4. 等待一段时间后关闭线程池
try {
Thread.sleep(10000); // 等待 10 秒钟,让任务有时间执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
scheduledExecutorService.shutdown();
}
}
六、注意事项
- 线程池大小设置:根据系统的硬件资源(如CPU核心数)和任务的类型,适当调整线程池的大小。线程池过大会带来资源浪费,过小则可能导致任务执行延迟。
- 合理选择队列:使用适当的队列类型(如
LinkedBlockingQueue
、ArrayBlockingQueue
)可以优化线程池性能。 - 线程池监控与调试:在生产环境中,监控线程池的状态(如活跃线程数、等待任务数等)对于调优非常重要。
总结
Java 线程池是处理多线程并发任务的重要工具,能够有效管理线程的生命周期,提高性能和资源利用率。通过合理配置线程池参数和任务队列,可以根据业务需求优化线程池的性能。在实际开发中,需要根据任务的特点和系统的资源状况,选择合适的线程池类型和配置。