在 Java 并发编程的领域中,线程的管理是一个核心且复杂的问题。手动创建和销毁线程不仅会增加代码的复杂性,还可能导致资源浪费、性能下降等问题。而Executor 框架的出现,就像一位高效的 “线程管家”,为我们提供了一套完善的线程管理机制。。
Executor 是 Java 5 中引入的一个接口,它位于java.util.concurrent包下,是 Java 并发框架的核心组成部分。简单来说,Executor 框架是一套用于管理线程的工具,它将线程的创建、执行、销毁等操作进行了封装,让开发者无需直接与线程打交道,只需关注任务的定义和提交即可。打个比方,Executor 就像是一个线程池的 “调度中心”,我们把需要执行的任务提交给它,它会根据内部的线程池资源来安排任务的执行,而我们不需要关心具体是哪个线程执行了任务,也不需要手动去创建和关闭线程。
Executor 框架的主要作用在于对线程进行高效管理,具体体现在以下几个方面:
(1)简化线程管理:开发者无需手动创建Thread对象,也无需调用start()方法启动线程,只需创建任务(实现Runnable或Callable接口)并提交给 Executor 即可,大大简化了并发编程的代码。
(2)提高资源利用率:通过线程池复用线程,避免了频繁创建和销毁线程所带来的开销。线程的创建和销毁需要消耗系统资源,而复用线程可以减少这些操作,提高系统的资源利用率。
(3)控制并发数量:Executor 框架可以通过配置线程池的参数,如核心线程数、最大线程数等,来控制同时运行的线程数量,防止因线程过多而导致的系统资源耗尽问题。
(4)支持任务排队和调度:当提交的任务数量超过线程池的处理能力时,Executor 会将任务放入队列中等待执行。同时,它还支持定时任务、周期性任务等调度功能。
Executor 框架的核心原理是基于线程池实现的,其主要由以下几个部分组成:
(1)Executor 接口:该接口是整个框架的基础,它只定义了一个方法execute(Runnable command),用于提交一个Runnable任务供执行。
(2)ExecutorService 接口:它继承了 Executor 接口,提供了更丰富的方法,如提交任务并返回结果的submit()方法、关闭线程池的shutdown()和shutdownNow()方法等。
(3)ThreadPoolExecutor 类:这是 Executor 框架的核心实现类,它实现了 ExecutorService 接口,负责管理线程池的创建、任务的执行、线程的复用等具体操作。
(4)Executors 工具类:提供了一系列静态方法,用于快速创建不同类型的线程池,如固定大小的线程池、缓存线程池、单线程线程池等。
其工作流程大致如下:
(1)当开发者提交一个任务(Runnable或Callable)给 Executor 时,Executor 会先检查核心线程池是否有空闲线程。
(2)如果有空闲的核心线程,则直接使用该线程执行任务。
(3)如果没有空闲的核心线程,且当前线程数未达到最大线程数,则创建新的线程执行任务。
(4)如果当前线程数已达到最大线程数,则将任务放入阻塞队列中等待。
(5)当阻塞队列已满,且当前线程数未超过最大线程数时,会创建非核心线程执行任务;如果线程数已达到最大线程数,则会根据拒绝策略处理该任务(如抛出异常、丢弃任务等)。
(6)当线程完成任务后,会从队列中获取新的任务继续执行,实现线程的复用。
Executor 的优点主要有:
(1)提高性能:通过线程复用减少了线程创建和销毁的开销,提高了系统的性能。
(2)简化代码:将线程管理的细节封装起来,开发者只需关注任务的实现,简化了并发编程的代码。
(3)控制资源:可以通过配置线程池参数来控制线程的数量,避免资源耗尽。
(4)支持多种任务类型:不仅支持Runnable任务,还通过submit()方法支持Callable任务,Callable任务可以返回执行结果,更灵活。
当然,其也有不少缺点,例如:
(1)线程池参数配置复杂:线程池的性能很大程度上依赖于核心线程数、最大线程数、队列大小等参数的配置,如果配置不当,可能会导致性能下降甚至出现问题。
(2)可能导致任务延迟:当任务数量过多,超过线程池的处理能力时,任务会在队列中等待,可能导致任务执行延迟。
(3)存在内存泄漏风险:如果线程池中的线程是通过ThreadFactory创建的,且线程持有外部对象的引用,当线程池未被正确关闭时,可能会导致内存泄漏。
(4)不适合短期任务过多的场景:对于大量非常短的任务,线程池的管理开销可能超过其带来的好处。
下面通过代码示例来演示 Executor 框架的使用。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 使用 Executors 工具类创建线程池
public class ExecutorExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池,核心线程数和最大线程数都为3
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交5个任务给线程池
for (int i = 0; i < 5; i++) {
final int taskId = i;
// 提交Runnable任务
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务" + taskId + "正在执行,执行线程:" + Thread.currentThread().getName());
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务" + taskId + "执行完毕");
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
/*
该代码使用Executors.newFixedThreadPool(3)创建了一个固定大小为 3 的线程池。然后提交了 5 个任务,线程池会用 3 个线程来执行这些任务,当有线程完成任务后,会去执行队列中等待的任务。最后调用shutdown()方法关闭线程池。
*/
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
// 使用 ThreadPoolExecutor 自定义线程池
public class ThreadPoolExecutorExample {
public static void main(String[] args) {
// 自定义线程池参数
int corePoolSize = 2; // 核心线程数
int maximumPoolSize = 4; // 最大线程数
long keepAliveTime = 5; // 非核心线程空闲时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2); // 阻塞队列
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue
);
// 提交6个任务
for (int i = 0; i < 6; i++) {
final int taskId = i;
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务" + taskId + "正在执行,执行线程:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
// 关闭线程池
executor.shutdown();
}
}
/*
该代码通过ThreadPoolExecutor构造方法自定义了线程池的参数。核心线程数为 2,最大线程数为 4,阻塞队列大小为 2。当提交 6 个任务时,前 2 个任务由核心线程执行,接下来的 2 个任务进入队列等待,最后 2 个任务会创建非核心线程执行(因为此时队列已满,且未达到最大线程数)
*/
通过以上代码示例,我们可以直观地感受到 Executor 框架在管理线程和执行任务方面的便捷性。在实际开发中,我们需要根据具体的业务场景选择合适的线程池类型,并合理配置线程池参数,以达到最佳的性能效果。