文章目录
一、Java线程池简介
在计算机编程中,线程池是一种预先创建的线程集合,用于执行多个任务。线程池可以提高线程的利用率,避免频繁地创建和销毁线程。
二、线程池的基本组成
1. 线程池类及其功能
现根据这个demo跑通,可以看到在这个demo中在主线函数的线程启动后有启动了一个新的线程。
// 创建线程池对象
ExecutorService threadPool = Executors.newSingleThreadExecutor();
public class Main {
public static void main(String[] args) {
// 创建任务
Runnable task1 = new Task();
System.out.println("main函数线程名称:"+Thread.currentThread().getName());
// 创建只有一个线程的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 提交任务
threadPool.execute(task1);
// 关闭线程池
threadPool.shutdown();
}
}
public class Task implements Runnable {
@Override
public void run() {
System.out.println("新启动的线程:"+Thread.currentThread().getName());
}
}
2. 线程池参数的设置
线程池参数配置一共有七项
- corePoolSize – 要保留在池中的线程数,即使它们处于空闲状态,除非 allowCoreThreadTimeOut 已设置
- maximumPoolSize – 池中允许的最大线程数
- keepAliveTime – 当线程数大于核心数时,这是多余的空闲线程在终止之前等待新任务的最长时间。 - unit – 参数的时间 keepAliveTime 单位
- workQueue – 用于在执行任务之前保留任务的队列。此队列将仅 Runnable 保存该方法提交 execute 的任务。
- threadFactory – 执行器创建新线程时使用的工厂
- handler – 由于达到线程边界和队列容量而被阻止执行时要使用的处理程序
当设置线程池参数时,可以使用以下代码示例来说明参数的取值范围和用法:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// corePoolSize: 设置保留在池中的线程数
int corePoolSize = 5;
// maximumPoolSize: 设置允许的最大线程数
int maximumPoolSize = 10;
// keepAliveTime: 空闲线程等待新任务的最长时间
long keepAliveTime = 1;
// unit: keepAliveTime 的时间单位
TimeUnit unit = TimeUnit.MINUTES;
// workQueue: 任务队列,用于保留任务
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
// threadFactory: 线程工厂,用于创建新线程
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// handler: 当线程边界和队列容量达到而被阻止执行时要使用的处理程序
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务给线程池执行
for (int i = 0; i < 20; i++) {
threadPoolExecutor.execute(() -> {
System.out.println("Task Executed by Thread: " + Thread.currentThread().getName());
});
}
// 关闭线程池
threadPoolExecutor.shutdown();
}
}
以上代码示例中,使用了 ThreadPoolExecutor
类来创建线程池,并设置了相关参数:
corePoolSize
: 设置为 5,表示线程池中保留的核心线程数为 5。maximumPoolSize
: 设置为 10,表示允许的最大线程数为 10。keepAliveTime
和unit
: 设置为 1 分钟,当线程池中的线程数量超过核心数时,空闲线程在终止之前等待新任务的最长时间为 1 分钟。workQueue
: 使用ArrayBlockingQueue
作为任务队列,容量为 50,该队列仅保存execute
方法提交的Runnable
任务。threadFactory
: 使用默认的线程工厂来创建新线程。handler
: 使用CallerRunsPolicy
处理程序,当线程边界和队列容量达到而被阻止执行时,由提交任务的线程来执行被阻止的任务。
通过调整这些参数的取值,可以根据实际需求来配置线程池的性能和行为。
三、线程池分类
线程池可以根据不同的需求进行分类,以下是几种常见的线程池分类:
-
固定大小线程池(FixedThreadPool):该线程池会创建固定数量的线程,并且线程数量一旦创建就不会改变。适用于需要处理固定数量任务的场景,例如服务器端的请求处理。
-
缓存线程池(CachedThreadPool):该线程池可以根据需要动态地创建线程,如果线程池中有空闲线程可用,则复用空闲线程,否则将创建新线程。适用于需要处理大量短期任务的场景,可以灵活地调整线程数量。
-
单线程线程池(SingleThreadExecutor):该线程池只会创建一个单线程来执行任务,按照任务的提交顺序依次执行。适用于需要保证任务按顺序执行的场景,例如日志记录或者任务队列消费。
-
定时线程池(ScheduledThreadPool):该线程池可以在指定的延迟时间或定时时间执行任务。适用于需要定时执行任务或者延迟执行任务的场景,例如定时任务调度、定时数据备份等。
线程池创建
注意:
阿里巴巴Java开发手册中不推荐使用 Executors
工具类来创建线程池,主要基于以下几个原因:
-
预定义的线程池配置有局限性:
Executors
提供的方法虽然方便,但默认情况下创建的线程池可能存在一些问题。例如,newFixedThreadPool
和newSingleThreadExecutor
创建的线程池使用的是无界队列,容易导致内存溢出。而newCachedThreadPool
和newScheduledThreadPool
的最大线程数是整型的最大值,这可能会导致创建过多的线程而耗尽系统资源。
这些都会导致oom(内存溢出)的问题
-
线程池大小缺乏合理的配置:
Executors
提供的方法缺少对线程池参数的明确配置。如核心线程数、最大线程数、等待时间等,未做明确的设置。这可能导致线程池的性能和资源利用率不佳。 -
潜在的风险:使用
Executors
创建的线程池可能隐藏了潜在的风险,例如任务堆积、资源耗尽等问题。这些问题在高并发场景下可能导致系统崩溃或性能下降。
基于以上原因,阿里巴巴Java开发手册建议开发者显式地使用 ThreadPoolExecutor
类来创建线程池,并根据具体的业务需求进行参数的合理配置,以保证线程池的性能、稳定性和可扩展性。
规范创建
使用ThreadPoolExecutor 类指定各种参数的大小配置,就像这样
四、线程池的执行流程
1. 任务提交与执行
// 提交任务到线程池
executor.submit(new Task());
// 提交任务到线程池
//executor.execute(new Task());
// 定义任务类
class Task implements Runnable {
public void run() {
// 任务的具体逻辑
}
}
execute和submit区别
在 Java 中,线程池的 submit()
和 execute()
方法都可用于向线程池提交任务,但它们存在一些区别。
-
返回值类型:
submit()
方法返回一个Future
对象,可以通过该对象获取任务的执行结果或者取消任务。而execute()
方法没有返回值,无法获取任务的执行结果。 -
异常处理:
submit()
方法可以捕获并处理任务执行过程中抛出的异常,通过Future
对象的get()
方法获取异常信息。而execute()
方法无法直接捕获任务执行过程中的异常,需要通过自定义的方式来处理。 -
任务类型:
submit()
方法可以接受Callable
和Runnable
两种类型的任务,即可以提交有返回值的任务(Callable
)和无返回值的任务(Runnable
)。而execute()
方法只能接受Runnable
类型的任务。 -
扩展性:由于
submit()
方法返回了Future
对象,可以通过Future
对象来取消任务、获取任务执行状态等,提供了更多的扩展性。而execute()
方法没有这些扩展性。
如果需要获取任务的执行结果、进行异常处理或者需要更多的扩展性,推荐使用 submit()
方法。如果只是简单地提交一个无返回值的任务,可以使用更简洁的 execute()
方法。
2.批量执行任务
使用线程池的invokeAll方法进行批量任务的执行,但是被执行任务需要实现接口Callable,下面是一个demo
import java.util.concurrent.Callable;
public class Task implements Callable<Integer> {
/**
* 任务编号
*/
private final int index;
public Task(int index) {
this.index = index;
}
@Override
public Integer call() throws Exception {
// 使当前线程休眠1秒钟
Thread.sleep(1000);
return index;
}
}
public class Main {
public static void main(String[] args) {
// 创建任务列表
List<Task> tasks = new ArrayList<>();
// 填充任务列表
for (int i = 1; i <= 10; i++) {
// 添加任务
tasks.add(new Task(i));
}
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
// 提交任务列表
List<Future<Integer>> futures = threadPool.invokeAll(tasks);
// 遍历执行结果列表
for (Future<Integer> future : futures) {
// 输出执行结果
System.out.println(future.get());
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// 关闭线程池
threadPool.shutdown();
}
}
}
3. 线程池的状态转换
线程池状态转换:
-
线程池的创建与初始化:线程池被创建时,初始状态为
NEW
,此时还没有线程被启动。当调用线程池的execute()
或submit()
方法时,如果当前线程数小于核心线程数,则会创建新的线程执行任务,此时线程池的状态变为RUNNING
。 -
线程池的运行:在线程池处于
RUNNING
状态时,可以接受任务并将其交给空闲线程处理。如果当前线程数超过了核心线程数,但小于最大线程数,那么会创建新的线程执行任务。如果超过了最大线程数,则任务会被放入阻塞队列中等待执行。当调用线程池的shutdown()
方法时,线程池的状态变为SHUTDOWN
,此时线程池不再接受新的任务提交,但仍会执行任务队列中已有的任务。如果调用shutdownNow()
方法,则线程池的状态变为STOP
,此时会停止所有正在执行的任务。 -
线程池的关闭:当所有任务执行完毕且任务队列为空时,线程池的状态会变为
TERMINATED
。此时线程池中的所有线程都已经被终止,不再占用系统资源。
需要注意的是,在线程池处于 SHUTDOWN
或 STOP
状态时,可以通过 awaitTermination()
方法来等待线程池中的任务执行完成或者超时。另外,在线程池关闭后,如果需要重新启动线程池,需要重新创建一个新的线程池实例。
五、简单总结
线程池的一些基础概念掌握对于使用线程池是一个很好的开始,因为当每个参数都明白其意义后在什么场景下使用什么样的线程池就显得游刃有余了。当然本人也是在学习的过程中,如有哪些遗漏还请指出。