1.Java 中的线程池是如何实现的
Java中的线程池主要通过java.util.concurrent
包提供的Executor
框架实现。线程池的核心是重用一组现有线程来执行任务,而不是为每个任务创建新线程。这样做可以减少因频繁创建和销毁线程带来的开销,提高系统资源的利用率,并提供更好的系统性能。在Java中,线程池的实现主要依赖于以下几个关键类:
1. Executor接口
Executor
是最基础的接口,它提供了一个execute(Runnable command)
方法,用于执行任务。然而,这个接口本身并不直接提供线程池功能。
2. ExecutorService接口
ExecutorService
是Executor
的子接口,提供了更丰富的线程池管理功能,包括任务提交、线程池关闭等。它定义了一系列方法,允许对任务的提交进行更细致的控制,并能返回任务的执行结果。
3. ThreadPoolExecutor类
ThreadPoolExecutor
是ExecutorService
接口的一个实现类,提供了线程池的实现。它允许配置线程池的核心参数,如核心线程数、最大线程数、线程存活时间、任务队列等。ThreadPoolExecutor
是实现线程池的直接方式。
4. ScheduledExecutorService接口
ScheduledExecutorService
是ExecutorService
的子接口,提供了任务的定时执行和周期性执行功能。
5. ScheduledThreadPoolExecutor类
ScheduledThreadPoolExecutor
是ScheduledExecutorService
接口的实现类,支持任务的定时执行和周期性执行。
实现原理
- 任务队列:线程池使用一个阻塞队列来存储待执行的任务。当所有线程都忙时,新提交的任务会被放入队列中等待。
- 线程管理:线程池根据配置的参数(如核心线程数、最大线程数)来创建和管理线程。当任务提交给线程池时,如果核心线程都在忙且任务队列未满,则任务会被加入队列。如果核心线程都在忙且任务队列已满,线程池会创建新的线程来处理任务,直到线程数达到最大线程数限制。
- 线程回收:当线程空闲时间超过配置的存活时间时,线程池可能会回收线程以节省资源。
示例
创建固定大小的线程池并提交任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadPoolDemo {public static void main(String[] args) {// 创建一个固定大小的线程池ExecutorService executor = Executors.newFixedThreadPool(4);// 提交任务给线程池执行for (int i = 0; i < 10; i++) {int taskId = i;executor.execute(() -> {System.out.println("Executing task " + taskId + " inside : " + Thread.currentThread().getName());});}// 关闭线程池executor.shutdown();}
}
在这个示例中,通过Executors.newFixedThreadPool(4)
创建了一个固定大小为4的线程池,然后提交了10个任务。线程池会重用这4个线程来执行所有提交的任务。
2.创建线程池的几个核心构造参数
在Java中,通过ThreadPoolExecutor
类创建线程池时,需要指定几个核心构造参数,这些参数对线程池的行为和性能有重要影响。以下是这些核心构造参数:
1. corePoolSize
- 类型:
int
- 描述:线程池的基本大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了之后才会创建超过这个数量的线程。
2. maximumPoolSize
- 类型:
int
- 描述:线程池允许的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
3. keepAliveTime
- 类型:
long
- 描述:当线程数大于核心线程数时,这是多余空闲线程在终止前等待新任务的最长时间。
4. unit
- 类型:
TimeUnit
- 描述:
keepAliveTime
参数的时间单位。TimeUnit
是一个枚举,提供了如TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
等时间单位。
5. workQueue
- 类型:
BlockingQueue<Runnable>
- 描述:用来存储等待执行的任务的阻塞队列。可以选择不同类型的队列,如
LinkedBlockingQueue
、ArrayBlockingQueue
等,不同类型的队列在处理不同类型的任务时效率不同。
6. threadFactory(可选)
- 类型:
ThreadFactory
- 描述:用于设置创建线程的工厂。可以通过实现
ThreadFactory
接口自定义线程创建方式(如设置线程名、优先级等)。
7. handler(可选)
- 类型:
RejectedExecutionHandler
- 描述:当任务无法被线程池执行时(例如,队列已满且线程数已达到最大值),拒绝任务的处理程序。常见的有四种策略:
ThreadPoolExecutor.AbortPolicy
(抛出异常)、ThreadPoolExecutor.CallerRunsPolicy
(调用者运行任务)、ThreadPoolExecutor.DiscardPolicy
(忽略任务)、ThreadPoolExecutor.DiscardOldestPolicy
(丢弃队列最前面的任务,然后重试执行任务)。
示例
import java.util.concurrent.*;public class ThreadPoolExample {public static void main(String[] args) {int corePoolSize = 5;int maximumPoolSize = 10;long keepAliveTime = 5000;TimeUnit unit = TimeUnit.MILLISECONDS;BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());executor.execute(() -> {System.out.println("Task is running.");});executor.shutdown();}
}
在这个示例中,创建了一个ThreadPoolExecutor
实例,并指定了核心构造参数,然后提交了一个简单的任务来执行。
3.线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?线程池中线程的创建方式主要取决于线程池的配置参数和实际运行时的任务需求。线程池中线程的创建和管理遵循以下基本规则:
初始化核心线程
- 立即创建:在某些线程池配置中,可以选择在线程池创建时立即初始化并启动核心线程数(
corePoolSize
),即使没有任务提交。这可以通过调用ThreadPoolExecutor
的prestartCoreThread()
或prestartAllCoreThreads()
方法实现。 - 按需创建:默认情况下,核心线程是按需创建的,即只有在任务被提交并且当前运行的线程数小于
corePoolSize
时,才会创建和启动新的线程。
超过核心线程数的线程创建
- 当所有核心线程都在忙碌,并且内部任务队列已满时,如果当前线程总数小于最大线程数(
maximumPoolSize
),线程池会创建新的线程来处理任务。 - 这些超出核心线程数的线程在空闲时会存在一段时间(由
keepAliveTime
参数控制),如果在这段时间内没有新的任务分配给它们,它们将被回收。
线程创建的实际流程
- 任务提交:当一个任务被提交到线程池时,线程池会进行如下判断:
- 如果当前运行的线程数小于
corePoolSize
,则创建并启动一个新的线程来执行提交的任务(即使其他工作线程空闲)。 - 如果当前运行的线程数等于或大于
corePoolSize
,但任务队列未满,任务会被加入队列等待执行。 - 如果任务队列已满,并且当前运行的线程数小于
maximumPoolSize
,线程池会尝试创建新的线程来直接执行任务。 - 如果当前运行的线程数达到
maximumPoolSize
,新提交的任务会被拒绝执行处理器处理。
- 如果当前运行的线程数小于
通过这种机制,线程池可以有效地管理线程的创建和销毁,以适应不同的工作负载,避免在高负载下创建过多的线程导致资源耗尽,同时也避免在低负载时保持过多的空闲线程导致资源浪费。