阿里java规范规定
阿里java规范第六节并发处理第4点提到线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。
下面我们研讨下为啥这么说。要搞清楚这个问题,我们就得了解Executors和ThreadPoolExecutor这两者的作用和区别:
Executors
Executors
类是 java.util.concurrent
包下的一个工具类,它提供了一系列工厂方法来创建不同类型的线程池。使用线程池可以复用线程,减少线程创建和销毁的开销,并且可以控制并发执行的线程数量,从而优化程序的性能和资源利用率。
以下是几种常见的线程池类型及其使用示例:
-
FixedThreadPool
固定大小的线程池。如果所有线程都处于活动状态,新任务将等待空闲线程可用。
代码演示
public static void main(String[] args) {
// 创建一个固定大小的线程池,包含3个线程
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务
for (int i = 0; i < 10; i++) {
final int index = i;
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " : " + index);
});
}
// 关闭线程池
executor.shutdown();
}
运行结果如下
pool-1-thread-1 : 0
pool-1-thread-2 : 1
pool-1-thread-1 : 3
pool-1-thread-2 : 4
pool-1-thread-3 : 2
pool-1-thread-2 : 6
pool-1-thread-2 : 8
pool-1-thread-2 : 9
pool-1-thread-1 : 5
pool-1-thread-3 : 7
-
SingleThreadExecutor
单线程的线程池。它用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
代码演示
public static void main(String[] args) {
// 创建一个单线程的线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交任务
for (int i = 0; i < 10; i++) {
final int index = i;
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " : " + index);
});
}
// 关闭线程池
executor.shutdown();
}
执行结果如下
pool-1-thread-1 : 0
pool-1-thread-1 : 1
pool-1-thread-1 : 2
pool-1-thread-1 : 3
pool-1-thread-1 : 4
pool-1-thread-1 : 5
pool-1-thread-1 : 6
pool-1-thread-1 : 7
pool-1-thread-1 : 8
pool-1-thread-1 : 9
-
CachedThreadPool
可缓存的线程池。如果线程池中的线程数量超过了处理任务所需要的线程,那么它就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能地添加新线程来处理任务。
代码演示
public static void main(String[] args) {
// 创建一个可缓存的线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 提交任务
for (int i = 0; i < 10; i++) {
final int index = i;
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " : " + index);
});
}
// 关闭线程池(通常不建议显式关闭CachedThreadPool)
executor.shutdown();
}
运行结果如下
pool-1-thread-1 : 0
pool-1-thread-6 : 5
pool-1-thread-7 : 6
pool-1-thread-4 : 3
pool-1-thread-8 : 7
pool-1-thread-10 : 9
pool-1-thread-5 : 4
pool-1-thread-3 : 2
pool-1-thread-2 : 1
pool-1-thread-9 : 8
-
ScheduledThreadPool
支持定时及周期性任务执行的线程池。
代码演示
public static void main(String[] args) {
// 创建一个ScheduledExecutorService
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
// 安排任务在指定延迟后执行
executor.schedule(() -> System.out.println("Delayed task"), 2, TimeUnit.SECONDS);
// 安排任务周期性执行
executor.scheduleAtFixedRate(() -> System.out.println("Periodic task"), 0, 1, TimeUnit.SECONDS);
// 关闭线程池
executor.shutdown();
}
执行结果如下
Periodic task
Periodic task
Delayed task
Periodic task
Periodic task
Periodic task
Periodic task
Periodic task
Executors弊端
在Java中使用Executors
类来创建线程池确实带来了很多便利,比如简化了线程池的创建和管理过程。然而,不当地使用Executors
也会带来一些弊端,主要包括以下几个方面:
-
资源耗尽风险:
Executors.newCachedThreadPool()
:这个线程池会创建新的线程来处理每个任务,如果任务提交速度超过了任务处理速度,就会创建大量线程,可能导致系统资源耗尽(如CPU和内存),进而引发OutOfMemoryError
或系统响应缓慢。
-
任务堆积和延迟:
Executors.newFixedThreadPool(int nThreads)
:这个线程池有固定数量的线程,如果所有线程都在忙碌,新提交的任务会被放在队列中等待。如果任务提交速度持续高于处理速度,任务队列会不断增长,可能导致任务处理延迟甚至超时。
-
无界队列问题:
默认情况下,Executors
创建的固定大小线程池和单线程池使用的队列是LinkedBlockingQueue
,这个队列的默认容量是Integer.MAX_VALUE
,即几乎是无界的。这意味着如果线程池处理速度跟不上任务提交速度,队列会无限增长,消耗大量内存,甚至导致OutOfMemoryError
。
-
缺乏灵活性:
Executors
提供的线程池配置相对固定,对于复杂的并发需求,可能无法提供足够的灵活性。例如,你可能需要自定义线程工厂、拒绝策略或队列类型等。
-
难以调试和监控:
使用Executors
创建的线程池,其内部实现细节(如线程命名、任务队列等)可能不够直观,这增加了调试和监控的难度。在生产环境中,了解线程池的状态和性能对于问题排查和优化至关重要。
-
线程安全问题:
尽管Executors
本身提供了线程安全的线程池实现,但开发者在使用线程池时仍需注意线程安全问题。例如,如果任务本身不是线程安全的,或者多个任务之间共享了可变状态但没有适当的同步机制,就可能导致数据不一致或竞态条件。
-
默认拒绝策略可能不适用:
当线程池无法处理新任务时(如队列已满且所有线程都在忙碌),会调用拒绝策略。Executors
创建的线程池默认使用ThreadPoolExecutor.AbortPolicy
,该策略会抛出RejectedExecutionException
。在某些情况下,这种策略可能不是最佳选择,例如,你可能希望将任务放入另一个队列、记录日志或执行其他回退操作。
ThreadPoolExecutor
ThreadPoolExecutor 是 Java 中用于创建和管理线程池的一个类,它位于 java.util.concurrent
包下。通过 ThreadPoolExecutor,你可以指定线程池的核心线程数、最大线程数、非核心线程空闲存活时间、时间单位、任务队列以及线程工厂和拒绝策略等参数,以创建符合你需求的线程池。
代码演示
public static void main(String[] args) {
// 1. 创建任务队列:这里使用有界队列
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(10);
// 2. 创建 ThreadPoolExecutor
// 参数依次为:核心线程数、最大线程数、非核心线程空闲存活时间、时间单位、任务队列、线程工厂、拒绝策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
1L, // 非核心线程空闲存活时间
TimeUnit.SECONDS, // 时间单位
workQueue, // 任务队列
Executors.defaultThreadFactory(), // 线程工厂,这里使用默认的
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略,当任务队列和线程池都满了之后的拒绝策略
);
// 3. 提交任务到线程池
for (int i = 0; i < 15; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " is processing " + taskId);
try {
// 模拟任务执行时间
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 4. 关闭线程池(不再接受新任务,但已提交的任务会继续执行)
executor.shutdown();
// 如果想要立即停止所有正在执行的任务,并且不再处理队列中等待的任务,可以使用shutdownNow()
// List<Runnable> droppedTasks = executor.shutdownNow();
// System.out.println("Dropped tasks: " + droppedTasks.size());
}
执行结果如下:
pool-1-thread-1 is processing 0
pool-1-thread-5 is processing 14
pool-1-thread-2 is processing 1
pool-1-thread-3 is processing 12
pool-1-thread-4 is processing 13
pool-1-thread-3 is processing 2
pool-1-thread-5 is processing 3
pool-1-thread-4 is processing 6
pool-1-thread-1 is processing 5
pool-1-thread-2 is processing 4
pool-1-thread-3 is processing 7
pool-1-thread-2 is processing 8
pool-1-thread-5 is processing 9
pool-1-thread-4 is processing 10
pool-1-thread-1 is processing 11
在这个例子中,我们创建了一个具有 2 个核心线程、5 个最大线程、非核心线程空闲存活时间为 1 秒、使用有界队列(容量为 10)的线程池。我们还指定了默认的线程工厂和拒绝策略(当任务队列和线程池都满了之后,尝试提交新任务时会抛出 RejectedExecutionException
)。
注意:
- 线程池中的线程数量会动态地根据任务的数量和队列的容量进行调整,但不会超过最大线程数。
- 当调用
shutdown()
方法时,线程池不再接受新任务,但会等待所有已提交的任务(包括队列中等待的任务)执行完成。 - 当调用
shutdownNow()
方法时,线程池会尝试停止所有正在执行的任务,并且不再处理队列中等待的任务,它会返回一个列表,包含那些被丢弃的任务。
总结:
综合上面的说明,建议在使用Executors
时,根据实际需求选择合适的线程池类型和配置,并考虑使用ThreadPoolExecutor
构造函数来创建线程池,以便更灵活地控制线程池的行为。同时,也需要注意线程池的状态监控和性能调优,以确保系统的稳定性和高效性。