目录
导言:
虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效。线程池就是为了解决这个问题。 如果某个线程不再使用了,并不是真正把线程释放, 而是放到一个 "池子" 中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了。
正文:
1.概念
线程池是一种用于管理和复用线程的技术,它可以帮助我们更有效地管理线程的生命周期,提高程序的性能和资源利用率。在Java中,线程池通常通过是java.util.concurrent.ExecutorService接口及其实现类来完成的。
2.线程池的组成和基本原理
Java线程池主要由以下几个部分组成:
- 任务队列(Task Queue):
用于存放待执行的任务,例如无界队列LinkedBlockingQueue、有界队列ArrayBlockingQueue等。
- 工作线程(Worker Threads):
工作线程是线程池中实际执行任务的线程。
线程池中的线程,用于执行任务队列中的任务。
- 线程池管理器(ThreadPool Manager):
用于管理线程池的生命周期,负责线程的创建、销毁和管理。
- 拒绝策略(Rejected Policy):
拒绝策略定义了当线程池无法接收新任务时的处理方式。当任务队列已满且线程池中的线程数已达到最大线程数时,拒绝策略会决定如何处理无法执行的任务,可以是抛出异常、丢弃任务或执行其他处理方式。
工作原理:
当一个任务提交给线程池时,线程池会根据当前的工作线程数和任务队列的状态来决定如何执行这个任务。如果工作线程数小于核心线程数,线程池会创建新的线程来执行任务;如果工作线程数等于核心线程数,任务会被放入任务队列等待执行;如果任务队列满了且工作线程数小于最大线程数,线程池会创建新的线程来执行任务;如果任务队列满了且工作线程数等于最大线程数,新提交的任务会根据拒绝策略进行处理。
3.使用ThreadPoolExecutor创建线程池
ThreadPoolExecutor是Java中用于自定义线程池的类,它实现了ExecutorService接口,提供了更灵活的线程池配置和管理,但是这个类的参数有点复杂,和线程池的组成息息相关。
ThreadPoolExecutor的构造函数定义如下:
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
- corePoolSize:线程池的基本大小,即就算线程是空闲的,线程池也会保留在池中的线程数。
- maximumPoolSize:线程池允许创建的最大线程数。
- keepAliveTime:当线程数大于核心线程数时,这是多余空闲线程在终止前等待新任务的最长时间,超过这个时间的空闲线程会被回收。
- unit:时间单位,用于指定keepAliveTime的单位。
- workQueue:用于在执行任务之前保存任务的队列。这个队列将仅保存Runnable任务。
- threadFactory:线程工厂,用于创建新线程。如果没有提供,默认使用Executors.defaultThreadFactory()。
- handler:拒绝策略,由于达到线程界限和队列容量而不能执行任务时所使用的处理程序。如果没有提供,默认使用ThreadPoolExecutor.AbortPolicy()。
内置的拒绝策略主要包含以下几种:
-
AbortPolicy:这是默认的拒绝策略。如果任务无法提交,将会抛出
RejectedExecutionException
异常。这种策略对于提交任务的线程是一个阻塞,因为它需要等待捕获并处理这个异常。 -
CallerRunsPolicy:在这种策略下,如果任务无法提交给线程池,那么将会在提交任务的线程(调用者线程)中执行这个任务。这种方式可以保证任务最终会被执行,但是可能会影响性能,因为任务是在调用者线程而不是在线程池线程中执行的。
-
DiscardPolicy:在这种策略下,如果任务无法提交给线程池,那么这个任务将被直接丢弃,不会产生任何异常。这种策略可能会导致任务的丢失,但是可以避免因任务过多而导致系统崩溃。
-
DiscardOldestPolicy:在这种策略下,如果任务无法提交给线程池,那么将会丢弃工作队列中等待时间最长的任务(即队列头部的任务),然后将新任务加入到工作队列的尾部。这种方式可以确保新任务有机会被执行,但是可能会导致一些较旧的任务被丢弃。
ThreadPoolExecutor的基本方法:
execute(Runnable command)
:执行一个任务。submit(Runnable task)
:提交一个任务,并返回一个Future
对象,可以通过这个对象来获取任务执行的结果或者取消任务。submit(Runnable task, T result)
:提交一个任务,并给定一个结果,当任务成功完成时,将结果与任务的输出关联起来。shutdown()
:开始关闭线程池,不再接受新任务,但会完成已提交的任务。awaitTermination(long timeout, TimeUnit unit)
:等待所有任务完成,或者超过指定的等待时间。isShutdown()
:检查线程池是否已关闭。isTerminated()
:检查线程池是否已终止,即所有任务都已完成。getActiveCount()
:获取当前活动的线程数(即正在执行任务的线程数)。
示例代码:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorExample {
public static void main(String[] args) {
// 创建一个线程池,核心线程数为2,最大线程数为4,最大空闲时间为30s,使用有界队列,超出核心线程数的任务会被放入队列中
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
// 提交5个任务给线程池执行
for (int i = 1; i <= 5; i++) {
//变量捕获
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
在这个示例中,我们创建了一个ThreadPoolExecutor实例,设置核心线程数为2,最大线程数为4,使用有界队列(ArrayBlockingQueue)来存储等待执行的任务。然后我们提交了5个任务给线程池执行,每个任务打印自己的任务ID并休眠1秒模拟任务执行。
结果如下:
4.使用Executors 创建常见的线程池
ThreadPoolExecutor的创建方式虽然比较自由但是过于繁琐,java中也可以使用Executors更简单的创建线程池。
1. 创建固定大小的线程池
Executors.newFixedThreadPool(int nThreads)方法创建了一个固定大小的线程池。这个线程池中的线程数量始终保持不变,如果某个线程因异常结束,会创建一个新线程来替代它。
int numberOfThreads = 5; // 指定线程池的大小
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(numberOfThreads);
// 提交任务到线程池
for (int i = 0; i < 10; i++) {
fixedThreadPool.submit(() -> {
System.out.println("Task " + i + " is running on thread " + Thread.currentThread().getName());
});
}
// 关闭线程池
fixedThreadPool.shutdown();
2. 创建单线程的执行器
Executors.newSingleThreadExecutor()方法创建了一个单线程的执行器。这个执行器确保所有任务按照指定顺序执行,因为它只有一个线程来执行任务。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 提交任务到执行器
singleThreadExecutor.submit(() -> {
System.out.println("Running the only task in the single-threaded executor");
});
// 关闭执行器
singleThreadExecutor.shutdown();
3. 创建可缓存的线程池
Executors.newCachedThreadPool()方法创建了一个可缓存的线程池。如果当前线程数超过了处理需求,将回收空闲的线程。当需求增加时,会创建新的线程,直到达到Integer.MAX_VALUE。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 提交任务到线程池
cachedThreadPool.submit(() -> {
System.out.println("Running a task in the cached thread pool");
});
// 通常不需要手动关闭可缓存的线程池,因为它会根据需要自动管理线程
4.创建定时任务的线程池
Executors.newScheduledThreadPool(int corePoolSize)方法创建了一个用于执行定时任务的线程池。这个线程池会根据任务的调度时间来执行任务。
int corePoolSize = 5; // 指定线程池的核心线程数
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(corePoolSize);
// 安排任务在未来的某个时间点执行
scheduledThreadPool.schedule(() -> {
System.out.println("Running a scheduled task");
}, 5, TimeUnit.SECONDS);
// 关闭线程池
scheduledThreadPool.shutdown();
可以看到使用Executors可以比较简单的创建线程池,但需要注意的是newFixedThreadPool(int nThreads)和newSingleThreadExecutor()创建的线程池在不再需要时应该调用shutdown()方法来关闭,以释放资源。而其它的方法不需要自己关闭,它们会自己调整。
总结:
线程池是多线程编程中常用的工具,能够有效管理线程并提高程序性能。合理使用线程池可以避免线程创建和销毁的开销,提高系统的稳定性和效率。线程池注意有两种创建方式,具体选择哪一种可以根据自己的需求来判断。