一、引言
在 Java 多线程编程中,线程池是一个极为重要的工具。它通过复用已有的线程,减少线程创建和销毁的开销,从而显著提高应用程序的性能和资源利用率。本文将深入探讨 Java 线程池的各个方面,帮助读者全面掌握线程池的使用。
二、线程池基础知识
2.1 什么是线程池
线程池可以理解为一个管理线程的 “池子”。它预先创建一定数量的线程并存储在池中,当有任务提交时,直接从线程池中取出线程来执行任务,任务完成后,线程并不会被销毁,而是返回线程池等待下一个任务。
2.2 为什么使用线程池
降低资源消耗:避免频繁创建和销毁线程带来的系统开销。创建线程需要分配内存、初始化栈等操作,这些操作在高并发场景下会消耗大量资源。
提高响应速度:由于线程已预先创建,任务提交后能立即得到执行,无需等待线程创建过程。
提高线程的可管理性:线程池可以对线程进行统一管理,如线程数量的控制、任务队列的管理等,便于监控和调优。
2.3 线程池的核心组件
线程池管理器:管理线程池的创建、销毁以及线程池状态的监控等。
工作线程:线程池中的实际执行任务的线程,它们从任务队列中获取任务并执行。
任务队列:用于存储等待执行的任务。当线程池中的线程都在忙碌时,新提交的任务会被放入任务队列中排队等待。
任务拒绝策略:当任务队列已满且线程池中的线程达到最大数量时,新提交的任务会被拒绝,此时需要有相应的拒绝策略来处理这些被拒绝的任务。
三、线程池的种类
3.1 newFixedThreadPool
创建一个固定大小的线程池,线程池中的线程数量始终保持不变。当有新任务提交时,如果线程池中有空闲线程,则立即执行任务;如果没有空闲线程,则将任务放入任务队列中等待。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
final int task = i;
fixedThreadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is handling task " + task);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
fixedThreadPool.shutdown();
3.2 newCachedThreadPool
创建一个可缓存的线程池,如果线程池中的线程在 60 秒内未被使用,将会被回收。当有新任务提交时,如果线程池中有空闲线程,则立即执行任务;如果没有空闲线程,则创建一个新线程来执行任务。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int task = i;
cachedThreadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is handling task " + task);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
cachedThreadPool.shutdown();
3.3 newSingleThreadExecutor
创建一个单线程的线程池,线程池中只有一个线程在工作。所有任务按照提交的顺序依次执行,相当于一个单线程的串行执行器。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int task = i;
singleThreadExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is handling task " + task);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
singleThreadExecutor.shutdown();
3.4 newScheduledThreadPool
创建一个支持定时及周期性任务执行的线程池。可以指定线程池的大小,线程池中的线程可以按照一定的延迟时间或周期来执行任务。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(() -> {
System.out.println("This task is scheduled to run after 3 seconds.");
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(() -> {
System.out.println("This task is scheduled to run every 2 seconds.");
}, 0, 2, TimeUnit.SECONDS);
scheduledThreadPool.shutdown();
四、线程池实践
4.1 自定义线程池
在实际应用中,我们通常会根据具体需求来自定义线程池,而不是直接使用Executors工厂类创建的线程池。通过ThreadPoolExecutor类可以创建自定义线程池。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
1, // 线程存活时间
TimeUnit.MINUTES, // 时间单位
new ArrayBlockingQueue<>(5), // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
for (int i = 0; i < 10; i++) {
final int task = i;
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is handling task " + task);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
threadPoolExecutor.shutdown();
4.2 线程池监控
为了确保线程池的正常运行和性能优化,对线程池进行监控是很有必要的。ThreadPoolExecutor类提供了一些方法来获取线程池的运行状态信息,如线程池中的活跃线程数、已完成任务数等。
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
// 获取活跃线程数
int activeCount = executor.getActiveCount();
// 获取已完成任务数
long completedTaskCount = executor.getCompletedTaskCount();
// 获取线程池中的线程数
int poolSize = executor.getPoolSize();
五、线程池使用注意事项
5.1 合理设置线程池参数
核心线程数:应根据任务的类型和数量来设置。如果任务是 CPU 密集型的,核心线程数一般设置为 CPU 核心数或略少;如果是 I/O 密集型的,可以适当增加核心线程数,因为 I/O 操作会使线程阻塞,增加线程数可以充分利用 CPU 资源。
最大线程数:要考虑系统的负载能力和资源限制,避免设置过大导致系统资源耗尽。
任务队列:选择合适的任务队列类型和大小。如果任务队列过小,可能会导致任务频繁被拒绝;如果过大,可能会占用过多内存。
5.2 避免死锁
在使用线程池时,要注意避免死锁的发生。死锁通常是由于线程之间相互等待对方释放资源而导致的。在设计任务和线程池时,要确保资源的获取和释放顺序是合理的,避免出现循环等待的情况。
5.3 正确处理任务异常
当任务在执行过程中发生异常时,要确保有合适的异常处理机制。如果不处理异常,可能会导致线程池中的线程终止,影响任务的正常执行。可以通过实现Thread.UncaughtExceptionHandler接口来捕获线程执行过程中的异常。
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.out.println("Thread " + t.getName() + " threw an exception: " + e.getMessage());
e.printStackTrace();
});
5.4 优雅关闭线程池
在应用程序结束时,要确保线程池能够优雅地关闭。调用shutdown()方法后,线程池会停止接受新任务,但会继续执行已提交到任务队列中的任务;调用shutdownNow()方法会尝试停止正在执行的任务,并立即清空任务队列。通常建议先调用shutdown()方法,等待一段时间后,如果线程池还未完全关闭,再调用shutdownNow()方法。
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 提交任务
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Pool did not terminate");
}
}
} catch (InterruptedException ie) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}