文章目录
- 1.线程池框架
- 2.线程池的优点
- 3.线程池的7个参数
- 5.线程池的队列
- 6.线程池的饱和策略
- 7.创建线程的几种方法
- 8.线程池的执行流程
- 9.线程池的不同应用场景
- 10.设计模式:适配器模式
- 11.Executors
- 12.ThreadPoolExecutor??
- 13.ScheduledThreadPoolExecutor??
- 13.1 框架
- 13.2 有Timer为什么需要ScheduledThreadPoolExecutor?
- 13.3 ScheduledThreadPoolExecutor 相比 ThreadPoolExecutor 的特性
- 13.4 ScheduledThreadPoolExecutor 的两个饱和策略及区别
- 13.5 两种关闭策略???
- 13.6 为什么 ThreadPoolExecutor 的调整策略不适用于 ScheduledThreadPoolExecutor
- 13.7 Executors 提供构造 ScheduledThreadPoolExecutor 的方法
- 13.8 ScheduledThreadPoolExecutor 中 scheduleAtFixedRate 和 scheduleWithFixedDelay 的区别
- 14.线程池和消息队列区别?如果消息队列积压消息,怎么处理?
1.线程池框架
Executor 是一个接口。它只定义了一个execute(Runnable command)方法,用于执行给定的Runnable任务。
Executors 是一个工具类,Executors.new…用于创建不同类型的内置的线程池(这些线程池都是实现了 Executor 接口)。
ThreadPoolExecutor是 核心实现类,它实现了ExecutorService接口。
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,并且实现了ScheduledExecutorService接口。
2.线程池的优点
线程池能够对线程进行统一分配,调优和监控:
(1)降低资源消耗:线程的创建和销毁是有成本的。当任务执行完毕后销毁线程,会频繁地进行线程上下文切换,消耗大量的 CPU 时间和内存资源。而线程池中的线程可以复用,当一个任务执行完成后,线程不会立即销毁,而是回到线程池中等待新的任务,从而减少了线程创建和销毁的开销。
(2)提高响应速度:新任务来时,无须创建线程。线程池中的线程已经存在并处于等待状态,一旦有任务,就可以立即执行。
(3)提高线程的可管理性:线程池可以统一管理线程的生命周期、任务分配和调度等。可以方便地设置线程池的大小等参数。
3.线程池的7个参数
(1)corePoolSize核心线程数
定义:线程池中的核心线程数量。在初始化线程池后,会创建corePoolSize个线程等待任务到来。
当提交一个任务时,如果当前运行的线程数小于corePoolSize,线程池会创建一个新线程来执行任务。这意味着只要任务数量不超过corePoolSize,每个任务都会有一个专门的线程来执行,不会将任务放入任务队列等待。
核心线程在没有任务时也不会被销毁(除非设置了allowCoreThreadTimeOut为true),它们会一直处于等待状态,随时准备执行新任务,这样可以快速响应新任务的提交,减少任务的等待时间,提高系统的响应速度。
(2)maximumPoolSize最大线程数
定义:线程池允许存在的最大线程数量。它限制了线程池在繁忙时期能够创建的线程总数。
当阻塞队列已满,且继续提交任务时,如果当前线程数小于maximumPoolSize,线程池会创建新线程来执行任务。这可以在任务队列无法容纳更多任务时,通过增加线程来处理额外的任务,避免任务积压。
当线程数达到maximumPoolSize且任务队列已满时,就需要根据饱和策略来处理新提交的任务。
如果maximumPoolSize设置过大,可能会导致系统资源(如内存、CPU 等)过度消耗,因为过多的线程会占用大量资源并可能导致频繁的上下文切换;如果设置过小,可能无法及时处理所有任务,导致任务响应时间变长或者任务被拒绝。
(3)keepAliveTime空闲线程存活时间
定义:当线程数大于corePoolSize时,多余的空闲线程在等待新任务的最长存活时间。
作用和影响:
这个参数的目的是在系统负载较低时,及时回收多余的线程,释放系统资源。当线程处于空闲状态且超过keepAliveTime没有新任务到来时,线程会被终止。
通过合理设置keepAliveTime,可以在保证系统能够处理突发任务高峰的同时,避免在低负载时期浪费资源。例如,如果keepAliveTime设置过长,可能会导致系统长时间保留一些不必要的空闲线程,占用系统资源;如果设置过短,可能会在任务高峰过后过快地销毁线程,当新任务到来时又需要重新创建线程,增加系统开销。
设置原则:
根据任务的频率和性质设置。对于频繁有任务的场景可以设置较短的时间,对于偶尔有任务的场景可以设置较长时间。例如,在一个处理定时任务的线程池中,如果任务每隔几分钟就会有一批,那么keepAliveTime可以设置为几分钟;如果任务是几个小时才会有一次,那么keepAliveTime可以设置为一个小时左右。
(4)unit(时间单位)
定义:用于指定keepAliveTime的时间单位,是一个枚举类型(TimeUnit),包括NANOSECONDS(纳秒)、MICROSECONDS(微秒)、MILLISECONDS(毫秒)、SECONDS(秒)、MINUTES(分钟)、HOURS(小时)等。
作用和影响:
它与keepAliveTime配合使用,准确地确定空闲线程的存活时长。例如,keepAliveTime为 60,unit为TimeUnit.SECONDS,表示空闲线程在 60 秒后如果没有新任务将被终止。
设置原则:
根据系统对时间精度的要求和任务的特点来选择。一般来说,在大多数情况下,SECONDS是一个比较合适的单位,既不会因为时间精度过高(如NANOSECONDS或MICROSECONDS)导致系统开销增加,也不会因为时间精度过低(如HOURS)而无法及时回收空闲线程。
(5)workQueue(任务队列)
定义:用于保存等待被执行的任务的阻塞队列。
它是线程池的一个重要组成部分,起到了缓冲任务的作用,使得任务可以在没有足够线程执行的情况下暂时存储。
(6)threadFactory(线程工厂)
定义:用于创建线程的工厂类。它是一个接口(ThreadFactory),通过实现这个接口可以自定义线程的创建过程。
作用和影响:
可以给每个新建的线程设置一个具有识别度的线程名,方便在调试和监控线程时使用。例如,可以在创建线程时为线程设置一个有意义的名字,如包含任务类型、线程编号等信息,这样在查看线程栈信息或者日志时,可以更容易地识别线程的用途。
还可以设置线程的其他属性,如线程的优先级、是否为守护线程等。不过,一般情况下,不建议随意设置线程优先级,因为这可能会导致线程调度的不公平性。
设置原则:
根据需要自定义线程的属性来设置。如果只是希望简单地为线程命名,方便调试,可以实现一个简单的ThreadFactory,在其中设置线程名;如果有特殊的线程属性要求(如设置为守护线程等),可以在ThreadFactory中进行相应的设置。
(7)handler(饱和策略)
定义:当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务。
4.参数如何设置
4.1线程池的核心线程数设置为多少合适?
核心线程池数量设置:可以参考 CPU 核心数。
如果是 CPU 密集型任务,核心线程数一般设置为 CPU 核心数 + 1,这样可以充分利用 CPU 资源,同时留一个线程用于处理其他可能的任务,避免 CPU 资源竞争导致性能下降。
对于 I/O 密集型任务,由于线程大部分时间在等待 I/O 操作完成,可以设置较多的核心线程数,如 CPU 核心数 * 2,因为 I/O 操作不会占用 CPU 资源,较多的线程可以同时等待 I/O,提高整体效率。
4.2核心线程数可以设置成0吗?
可以设置为 0,但这种情况比较特殊。
当核心线程数为 0 时,线程池在初始状态下没有线程,所有任务都会先放入任务队列,当有任务需要执行时,才会创建线程,直到线程数达到maximumPoolSize。
这种设置适用于任务提交频率较低,并且希望在没有任务时不占用线程资源的场景,但可能会导致任务响应时间变长,因为每次都需要先创建线程才能执行任务。
5.线程池的队列
设置原则:
根据任务的数量、任务的优先级要求、任务产生和消费的速度匹配情况等来选择。(1)如果任务数量可预测且有界,对执行顺序有要求,可以选ArrayBlockingQueue;
(2)如果任务数量不确定,希望尽量不拒绝任务,可以考虑LinkedBlockingQueue(注意容量控制);
(3)如果任务需要直接传递,生产和消费速度匹配,使用SynchronousQueue;
(4)如果任务有优先级之分,使用PriorityBlockingQueue。
5.1不同的任务队列
5.1.1 ArrayBlockingQueue:数组、有界
基于数组的有界阻塞队列,按 FIFO 顺序存储任务,线程安全,通过内部锁和条件变量实现阻塞和唤醒机制。
5.1.2 LinkedBlockingQueue:链表、无界
基于链表的阻塞队列,可以是有界或无界的(默认无界),也按 FIFO 顺序存储任务,吞吐量较高。
5.1.3 SynchronousQueue:不存储
一个线程生产的数据需要立刻被另一个线程消费
不存储元素的阻塞队列,每个插入操作必须等待另一个线程进行移除操作,元素在生产者和消费者之间直接传递,没有中间的存储环节。
5.1.4 PriorityBlockingQueue:无界、优先级
无界阻塞队列,任务按照优先级顺序被取出执行,任务需要实现Comparable接口或传入Comparator来确定优先级。
class Person implements Comparable<Person> {
private int age;
public Person(int age) {
this.age = age;
}
@Override
public int compareTo(Person other) {
return this.age - other.age;
}
}
class Book {
private String title;
public Book(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
public class ComparatorExample {
public static void main(String[] args) {
List<Book> books = new ArrayList<>();
books.add(new Book("Java核心技术"));
books.add(new Book("Effective Java"));
Comparator<Book> titleComparator = (b1, b2) -> b1.getTitle().compareTo(b2.getTitle());
Collections.sort(books, titleComparator);
for (Book book : books) {
System.out.println(book.getTitle());
}
}
}
5.2 任务队列如何保证线程安全
(1)ArrayBlockingQueue 内部使用了ReentrantLock(可重入锁)来确保在多线程环境下对队列操作的独占访问。
class ArrayBlockingQueue<E> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
// 其他变量和方法省略
public void put(E e) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
// 插入元素操作
items[putIndex] = e;
putIndex = (putIndex + 1) % items.length;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
}
(2)LinkedBlockingQueue 内部使用了两个ReentrantLock,一个用于控制插入操作(putLock),一个用于控制取出操作(takeLock)。这种设计使得插入和取出操作可以在一定程度上并行进行,提高了队列的吞吐量。
对于插入操作,当队列满时,线程会在put方法中通过putLock等待,直到队列有空间,利用notFull条件变量来实现阻塞和唤醒。对于取出操作,当队列空时,线程会在take方法中通过takeLock等待,直到队列中有元素,利用notEmpty条件变量来实现阻塞和唤醒。这样可以更精细地控制插入和取出操作的线程阻塞和唤醒,提高并发性能。
(3)SynchronousQueue 保证线程安全的方式:依赖于内部复杂的同步机制。
(4)PriorityBlockingQueue 内部通过一个平衡二叉树(具体是基于数组实现的二叉堆)来存储元素,并且使用ReentrantLock来保证线程安全。在插入和取出元素时,需要获取锁来操作这个二叉堆结构。
与其他阻塞队列类似,当队列为空时,取出元素的线程会被阻塞。它使用notEmpty条件变量来实现唤醒等待的线程。例如,在take方法中,线程获取锁后检查队列是否为空,如果为空则调用notEmpty.await()等待,当有新元素插入队列后,会调用notEmpty.signal()唤醒等待的线程。
5.3 同步队列和阻塞队列的区别
5.3.1 同步队列(SynchronousQueue):
它不存储元素,每个插入操作必须等待另一个线程进行移除操作,主要用于线程之间的直接任务传递,适用于生产者和消费者速度大致匹配的场景,吞吐量较高。
5.3.2 阻塞队列(ArrayBlockingQueue、LinkedBlockingQueue 、PriorityBlockingQueue):
阻塞队列是一种支持两个附加操作的队列。这两个操作是:**当队列满时,插入元素的线程会被阻塞;当队列空时,获取元素的线程会被阻塞。**它主要用于在生产者 - 消费者模式中,平衡生产者和消费者的速度差异,通过阻塞线程来避免资源的过度占用或任务丢失。
可以存储元素,当队列满时插入操作会被阻塞,当队列空时移除操作会被阻塞。用于在任务产生和任务执行速度不匹配时暂存任务,不同类型的阻塞队列有不同的存储和取出规则。
6.线程池的饱和策略
(1)AbortPolicy(默认) 抛异常
策略描述:当线程池的任务队列已满,并且线程数达到最大线程数(maximumPoolSize)时,如果继续提交新任务,会直接抛出RejectedExecutionException异常。这种策略是比较 “强硬” 的,它阻止任务提交者继续提交任务,确保系统能够及时发现线程池已经无法处理更多任务的情况。
适用场景:适用于对任务完整性要求很高,不允许任务丢失或者随意处理的场景。例如,在金融交易系统中,每一笔交易任务都非常重要,不能丢失或错误处理。如果线程池无法处理新的交易任务,抛出异常可以让上层应用及时捕获并采取相应的措施,如提示用户稍后重试或者进行系统扩容等。
(2)CallerRunsPolicy 让用户线程自己执行
策略描述:当线程池饱和时,新提交的任务不会被放入线程池的任务队列,也不会创建新的线程来执行,而是由提交任务的线程(即调用者线程)自己来执行这个任务。这种策略可以在一定程度上缓解线程池的压力,因为提交任务的线程在执行任务时会占用自身的资源,从而减缓新任务的提交速度,给线程池一些时间来处理积压的任务。
适用场景:适用于希望尽量处理任务,并且提交任务的线程可以分担一些任务处理的情况。例如,在 Web 应用中,当线程池用于处理用户请求饱和时,让用户请求线程直接执行任务,可以避免用户界面长时间无响应。这样虽然可能会导致用户请求处理速度变慢,但总比一直等待线程池有资源处理要好,同时也能让用户感受到任务的执行进度。
(3)DiscardOldestPolicy 淘汰最老的
策略描述:当线程池饱和时,会丢弃阻塞队列中最靠前(即等待时间最长)的任务,然后将新提交的任务放入队列或者创建新线程来执行(如果还有空闲线程)。这种策略认为新任务的优先级较高,或者旧任务可以被舍弃。
适用场景:适用于新任务的优先级较高,或者旧任务可以被舍弃的场景。例如,在实时数据处理系统中,新到来的数据可能比旧数据更重要,当线程池饱和时,可以丢弃队列中最旧的任务,优先处理新数据,以保证系统能够及时处理最新的信息。不过需要注意的是,这种策略可能会导致部分任务丢失,因此在使用时需要谨慎考虑任务的重要性和可丢弃性。
(4)DiscardPolicy 啥也不做
策略描述:当线程池饱和时,直接丢弃新提交的任务,不做任何处理。这种策略比较简单粗暴,它不会抛出异常,也不会尝试执行或保存被丢弃的任务。
适用场景:适用于对任务丢失不太敏感的场景。例如,在一个日志记录线程池中,如果日志记录任务过多,丢弃一些任务可能不会对系统的核心功能产生重大影响。因为日志记录主要是用于事后查看系统状态等辅助功能,即使丢失一些日志记录,在很多情况下也是可以接受的。
7.创建线程的几种方法
8.线程池的执行流程
8.1线程池创建阶段:
(1)通过Executors工具类创建
[newFixedThreadPool]:
方法介绍:通过Executors.newFixedThreadPool(int nThreads)创建一个固定大小的线程池。这个线程池中的线程数量固定为nThreads个,不会随着任务数量的变化而改变。
内部实现:它内部实际上是基于ThreadPoolExecutor实现的。将核心线程数(corePoolSize)和最大线程数(maximumPoolSize)都设置为nThreads,并使用一个无界的LinkedBlockingQueue作为任务队列。例如,ExecutorService executor = Executors.newFixedThreadPool(5);就创建了一个有 5 个线程的固定线程池。
[newSingleThreadExecutor]:
方法介绍:使用Executors.newSingleThreadExecutor()创建一个只有单个线程的线程池。这个线程池中的唯一线程会按照任务提交的顺序依次执行任务。
内部实现:同样是基于ThreadPoolExecutor构建,将核心线程数和最大线程数都设置为 1,任务队列采用无界的LinkedBlockingQueue。如果这个唯一的线程因为异常而终止,线程池会自动创建一个新的线程来继续执行后续任务。例如,ExecutorService singleExecutor = Executors.newSingleThreadExecutor();创建了单线程池。
[newCachedThreadPool]:
方法介绍:Executors.newCachedThreadPool()用于创建一个可缓存的线程池。这个线程池的线程数量会根据任务的情况动态变化。当有新任务提交且没有空闲线程时,会创建新的线程来执行任务;当线程空闲一段时间(默认 60 秒)后,会自动回收线程。
内部实现:它也是通过ThreadPoolExecutor实现的,将核心线程数设置为 0,最大线程数设置为Integer.MAX_VALUE,使用SynchronousQueue作为任务队列,使得任务不会被存储,而是直接由空闲线程或者新创建的线程来执行。例如,ExecutorService cachedExecutor = Executors.newCachedThreadPool();创建了可缓存线程池。
(2)使用 ThreadPoolExecutor 类创建。设置参数。
8.2 任务提交阶段:
当一个任务通过execute方法(Executor接口定义的方法,ExecutorService继承了该接口)或者submit方法(ExecutorService接口新增的用于提交有返回值任务的方法)提交给线程池时,线程池首先会检查当前运行的线程数是否小于corePoolSize。
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
ThreadPoolExecutor executorPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue
);
// 创建一个简单的Runnable任务
Runnable task = () -> {
System.out.println("任务正在执行");
};
// 提交任务到线程池,这就是任务提交阶段
executorPool.execute(task);
8.3 核心线程分配阶段:
如果当前运行的线程数小于corePoolSize,线程池会创建一个新的线程来执行这个任务,即使有空闲线程能够执行新来的任务,也会继续创建,直到线程数达到corePoolSize。新创建的线程会从任务队列中获取任务并执行,这个过程是通过Worker类(ThreadPoolExecutor内部用于包装线程和任务的类)来实现的。
// 模拟核心线程分配逻辑(实际是在ThreadPoolExecutor内部处理)
if (executorPool.getPoolSize() < corePoolSize) {
// 这里简单打印示意,实际会创建线程并执行任务
System.out.println("创建新的核心线程来执行任务");
}
8.4 任务队列存储阶段:
当核心线程分配完后,多余的任务会进入任务队列。
如果当前运行的线程数已经达到corePoolSize,那么线程池会将任务添加到任务队列(workQueue)中。
任务会在队列中等待,直到有空闲的线程来执行它。
不同类型的任务队列有不同的特性,例如ArrayBlockingQueue是有界队列,当队列满时,会根据线程池的饱和策略来处理后续任务;LinkedBlockingQueue可以是有界或无界的,无界情况下如果任务产生速度过快可能会导致内存问题。
8.5 线程扩充阶段:
**当任务队列已满,并且当前线程数小于maximumPoolSize时,线程池会创建新的线程来执行任务。**这些新线程同样会从任务队列中获取任务执行,直到线程数达到maximumPoolSize。
// 模拟线程扩充逻辑(实际由ThreadPoolExecutor内部根据情况处理)
if (workQueue.size() == workQueue.remainingCapacity() && executorPool.getPoolSize() < maximumPoolSize) {
System.out.println("任务队列已满,当前线程数小于最大线程数,创建新线程扩充线程池");
}
8.6 饱和策略执行阶段:
如果任务队列已满且线程数达到maximumPoolSize,此时再提交新任务,就会根据线程池设置的饱和策略(handler)来处理。例如,如果是AbortPolicy,会直接抛出RejectedExecutionException异常;如果是CallerRunsPolicy,会用提交任务的线程来执行任务;如果是DiscardOldestPolicy,会丢弃队列中最老的任务并执行新任务;如果是DiscardPolicy,会直接丢弃新任务。
8.7 任务执行阶段(线程从任务队列获取任务):
线程会不断地从任务队列中获取任务执行,直到任务队列空且线程数超过corePoolSize的部分线程空闲时间超过keepAliveTime。在这个阶段,线程通过循环调用getTask方法(ThreadPoolExecutor内部方法)来获取任务,获取到任务后,在Worker类的run方法中执行任务。
// 模拟线程从任务队列获取任务执行(实际在ThreadPoolExecutor内部Worker类等机制实现)
Thread thread = new Thread(() -> {
try {
Runnable task = workQueue.take();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
8.8 线程销毁阶段(线程空闲回收):
当线程数超过corePoolSize的部分线程空闲时间超过keepAliveTime,这些空闲线程会被终止,以释放系统资源。在getTask方法中,会根据线程池的状态和空闲时间来判断是否应该销毁线程。
在ThreadPoolExecutor的源码中,通过getTask方法来获取任务,在获取任务的过程中会检查线程是否应该被销毁。例如,在Worker类(用于包装线程和任务)的run方法中,会不断地循环获取任务并执行,如果获取不到任务并且满足销毁条件(线程数大于corePoolSize且空闲时间超过keepAliveTime),线程就会退出循环,进而被销毁。
9.线程池的不同应用场景
9.1 根据应用场景创建不同线程池类型(通过 Executors 工具类)
FixedThreadPool(固定大小线程池)
应用场景:适用于任务数量相对固定,且任务执行时间比较均匀的场景。例如,一个服务器需要处理固定数量的客户端连接请求,每个请求的处理时间大致相同。
创建方式和参数特点:通过Executors.newFixedThreadPool(int nThreads)创建。其内部将核心线程数(corePoolSize)和最大线程数(maximumPoolSize)都设置为nThreads,并使用一个无界的LinkedBlockingQueue作为任务队列。这种固定大小的线程池可以保证每个任务都能得到及时处理,并且线程资源不会因为请求数量的波动而频繁变化。
SingleThreadExecutor(单线程线程池)
应用场景:适用于需要保证任务执行顺序的场景,例如在日志记录系统中,为了确保日志按照产生的顺序依次记录,使用 SingleThreadExecutor 可以避免多个线程并发记录日志可能导致的顺序混乱问题。
创建方式和参数特点:使用Executors.newSingleThreadExecutor()创建。它的线程池中只有一个工作线程,所有提交的任务按照提交的顺序依次在这个单线程中执行,任务队列同样是无界的LinkedBlockingQueue。如果这个唯一的线程因为异常而终止,线程池会自动创建一个新的线程来继续执行后续任务。
CachedThreadPool(可缓存线程池)
应用场景:适用于处理大量短期异步任务的场景。例如,在一个 Web 服务器中处理大量短时间的 HTTP 请求,这些请求的到来时间和处理时间都不确定,CachedThreadPool 可以根据请求的数量动态地增加或减少线程数量,有效地利用系统资源,避免线程资源的浪费。
创建方式和参数特点:通过Executors.newCachedThreadPool()创建。它将核心线程数设置为 0,最大线程数设置为Integer.MAX_VALUE,使用SynchronousQueue作为任务队列,使得任务不会被存储,而是直接由空闲线程或者新创建的线程来执行,并且空闲线程会在 60 秒(默认)后被自动回收。
9.2 根据应用场景设置不同的参数
10.设计模式:适配器模式
适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。
在 Java 线程池中,适配器模式可以用于将不符合线程池要求的任务接口转换为线程池能够执行的Runnable接口。
public class ThreadPoolAdapterExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
MyTaskInterface myTask = new MyConcreteTask();
// 创建适配器对象,把自定义任务交给适配器
Runnable adapter = new MyTaskAdapter(myTask);
// 将适配器对象提交给线程池执行
executor.execute(adapter);
executor.shutdown();
}
}
//适配器
class MyTaskAdapter implements Runnable {
private MyTaskInterface myTask;//自定义任务,不符合接口要求
public MyTaskAdapter(MyTaskInterface myTask) {
this.myTask = myTask;
}
//适配器实现了Runnable接口
@Override
public void run() {
myTask.myTaskMethod();
}
}
解耦和复用:通过适配器模式,我们将任务的具体实现(MyTaskInterface及其实现类)和线程池的执行机制解耦。
遵循开闭原则:如果需要增加新的任务接口类型,我们只需要创建新的适配器类来适配新接口,而不需要修改线程池的代码,符合开闭原则,即对扩展开放,对修改关闭。
11.Executors
内置线程池
FixedThreadPool(固定大小)
特点:通过Executors.newFixedThreadPool(int nThreads)创建,它会创建一个包含固定数量线程的线程池。线程池中的线程数量一旦确定,就不会改变。如果所有线程都处于忙碌状态,新提交的任务会被存储在一个无界的LinkedBlockingQueue任务队列中等待执行。
适用场景:适用于处理任务数量相对固定,且任务执行时间比较均匀的场景。例如,一个服务器需要处理固定数量的客户端连接请求,每个请求的处理时间大致相同,使用 FixedThreadPool 可以保证每个请求都能得到及时处理,并且线程资源不会因为请求数量的波动而频繁变化。
SingleThreadExecutor(单线程线程池)
特点:使用Executors.newSingleThreadExecutor()创建,它的线程池中只有一个工作线程。所有提交的任务按照提交的顺序依次在这个单线程中执行,任务队列是无界的LinkedBlockingQueue。如果这个唯一的线程因为异常而终止,线程池会自动创建一个新的线程来继续执行后续任务。
适用场景:适用于需要保证任务执行顺序的场景,例如在日志记录系统中,为了确保日志按照产生的顺序依次记录,使用 SingleThreadExecutor 可以避免多个线程并发记录日志可能导致的顺序混乱问题。
CachedThreadPool(可缓存线程池)
特点:通过Executors.newCachedThreadPool()创建,它是一个可以根据需要自动创建新线程的线程池。
线程数量没有固定限制,空闲线程会在 60 秒(默认)后被自动回收。
它使用SynchronousQueue作为任务队列(不能存储任务,所以必须立刻执行),这意味着任务不会被存储,而是直接由空闲线程或者新创建的线程来执行。
适用场景:适用于处理大量短期异步任务的场景。例如,在一个 Web 服务器中处理大量短时间的 HTTP 请求,这些请求的到来时间和处理时间都不确定,CachedThreadPool 可以根据请求的数量动态地增加或减少线程数量,有效地利用系统资源,避免线程资源的浪费。
为什么不使用Executors来创建线程池?
因为使用无界队列,任务太多可能占用大量内存
12.ThreadPoolExecutor??
源码,没看。
shutdownNow会中断正在执行的任务。
13.ScheduledThreadPoolExecutor??
13.1 框架
ScheduledThreadPoolExecutor 内部构造了两个内部类 。ScheduledFutureTask 和DelayedWorkQueue。
13.1.1 ScheduledFutureTask:
继承了FutureTask,说明是一个异步运算任务;最上层分别实现了Runnable、Future、Delayed接口,说明它是一个可以延迟执行的异步运算任务。
13.1.2 DelayedWorkQueue:无界延迟队列
这是 ScheduledThreadPoolExecutor 为存储周期或延迟任务专门定义的一个延迟队列,继承了 AbstractQueue,为了契合 ThreadPoolExecutor 也实现了 BlockingQueue 接口。它内部只允许存储 RunnableScheduledFuture 类型的任务。与 DelayQueue 的不同之处就是它只允许存放 RunnableScheduledFuture 对象,并且自己实现了二叉堆(DelayQueue 是利用了 PriorityQueue 的二叉堆结构)。
13.2 有Timer为什么需要ScheduledThreadPoolExecutor?
在很多业务场景中,需要周期性地运行某项任务来获取结果,比如周期数据统计、定时发送数据等。随着业务量增大,可能需要多个工作线程运行任务来提升性能,或者需要更高的灵活性来控制和监控这些周期业务。
相较于早期的 Timer 类,它在多线程环境下能更好处理任务调度、灵活、可扩展。
13.3 ScheduledThreadPoolExecutor 相比 ThreadPoolExecutor 的特性
(1)任务类型ScheduledFutureTask 与执行
使用专门的任务类型 ScheduledFutureTask 来执行周期任务,也能接收不需要时间调度的任务(通过 ExecutorService 执行)。而 ThreadPoolExecutor 没有这种专门针对周期任务的任务类型设计。
(2)存储队列DelayedWorkQueue
使用专门的存储队列 DelayedWorkQueue 来存储任务,DelayedWorkQueue 是无界延迟队列 DelayQueue 的一种,且内部只允许存储 RunnableScheduledFuture 类型的任务。ThreadPoolExecutor 使用的是普通的阻塞队列(如 ArrayBlockingQueue、LinkedBlockingQueue 等),没有针对延迟和周期任务的特殊队列设计。
(3)执行机制简化
相比 ThreadPoolExecutor 简化了执行机制,例如其 delayedExecute 方法专门用于处理延迟任务的执行逻辑,在任务入队、启动线程等操作上有特定的逻辑。
(4)支持 run - after - shutdown 参数
支持可选的 run - after - shutdown 参数,在池被关闭(shutdown)之后支持可选的逻辑来决定是否继续运行周期或延迟任务。并且当任务(重新)提交操作与 shutdown 操作重叠时,复查逻辑也不相同。ThreadPoolExecutor 在关闭后通常不会再执行新提交的任务(除非使用特定的拒绝策略如 CallerRunsPolicy,但这与 ScheduledThreadPoolExecutor 的 run - after - shutdown 参数逻辑不同)。
13.4 ScheduledThreadPoolExecutor 的两个饱和策略及区别
直接拒绝:(类似于 ThreadPoolExecutor 的 AbortPolicy)
策略表现:当线程池的任务队列(DelayedWorkQueue)已满,并且所有工作线程都在忙碌执行任务时,新提交的任务会被拒绝。在这种情况下,ScheduledThreadPoolExecutor 会直接抛出RejectedExecutionException异常来通知调用者任务无法被接受。
适用场景:适用于对任务提交的准确性要求较高,不希望丢失任务信息的场景。例如,在一个对定时任务的执行顺序和完整性要求严格的系统中,如果线程池无法接受新任务,应该及时通知调用者,让调用者能够采取相应的措施,如调整任务的提交时间或者增加系统资源来扩大线程池的处理能力。
内部实现原理:在ScheduledThreadPoolExecutor的execute方法和相关调度方法中,当判断无法将任务成功放入队列或者安排执行时,就会抛出异常。因为ScheduledThreadPoolExecutor的任务队列DelayedWorkQueue是无界的,理论上只有在资源耗尽或者任务提交速度远超处理速度等极端情况下才会出现这种饱和状态。
等待队列(基于其自身的延迟队列特性)
策略表现:由于ScheduledThreadPoolExecutor使用的是DelayedWorkQueue作为任务队列,它是一个无界的延迟队列。
所以在正常情况下,任务会按照延迟时间或者周期安排被放入队列等待执行。即使当前所有线程都在忙碌,新的任务也会在队列中等待合适的时间来执行,不会立即被拒绝。这种方式类似于一种 “软性” 的饱和策略,通过队列来缓冲任务。
适用场景:适用于任务有延迟或周期执行要求,并且可以接受一定程度的任务积压的场景。
例如,在一个定时数据备份系统中,备份任务有特定的时间安排(如每天凌晨备份),新的备份任务可以在队列中等待合适的时间执行,即使当前线程正在处理其他备份任务,也不会丢失新任务,只要系统资源允许,最终这些任务都会按照计划执行。
内部实现原理:DelayedWorkQueue内部通过堆结构来维护任务的顺序,根据任务的延迟时间(对于延迟任务)或者下一次执行时间(对于周期任务)来排序。当线程有空闲时,会从队列头部取出下一个应该执行的任务进行处理。这样就保证了任务能够按照计划的时间顺序执行,同时利用队列的无界特性来容纳较多的任务。
13.5 两种关闭策略???
默认策略(类似 ThreadPoolExecutor 的一般关闭策略):
当调用 shutdown 方法时,线程池会拒绝新提交的任务,然后等待正在执行的任务和队列中的任务执行完成。在 ScheduledThreadPoolExecutor 中,它会根据 run - after - shutdown 参数来进一步处理队列中的任务。
onShutdown 方法中的特殊处理(基于 run - after - shutdown 参数):
在关闭线程池后,根据 run - after - shutdown 参数(continueExistingPeriodicTasksAfterShutdown 和 executeExistingDelayedTasksAfterShutdown)来决定是否取消并清除由于关闭策略不应该运行的任务。如果两个参数都为 false,会取消所有任务并清空队列;如果有一个为 true,会根据任务类型(周期或延迟)和是否已取消等条件来处理队列中的任务,例如对于已取消或根据参数不应运行的任务会从队列中移除并取消。
区别
处理时机:默认策略主要是在 shutdown 方法调用时开始拒绝新任务并等待现有任务完成;而 onShutdown 方法中的处理是在关闭线程池后的额外清理操作,根据参数来决定如何处理剩余任务。
任务处理方式:默认策略侧重于现有任务的正常执行完成;onShutdown 方法中的处理更侧重于根据参数对任务进行筛选和清理,包括取消不应运行的任务、移除已取消任务等操作。
13.6 为什么 ThreadPoolExecutor 的调整策略不适用于 ScheduledThreadPoolExecutor
(1)核心线程数和最大线程数的关联方式不同
在 ThreadPoolExecutor 中,核心线程数(corePoolSize)和最大线程数(maximumPoolSize)是紧密关联的,并且可以根据任务队列的状态(满或未满)来决定是否创建新线程,直到达到最大线程数。例如,当任务队列已满,且当前线程数小于最大线程数时,会创建新线程来处理任务。
而 ScheduledThreadPoolExecutor 是一个固定核心线程数大小的线程池,其最大线程数被固定为Integer.MAX_VALUE,且内部使用无界的DelayedWorkQueue作为任务队列。这意味着任务会不断地被放入队列中等待执行,不会出现像 ThreadPoolExecutor 中因为任务队列满而需要创建新线程的情况,所以调整maximumPoolSize对于 ScheduledThreadPoolExecutor 没有实际意义。
(2)任务队列的性质差异
ThreadPoolExecutor 可以使用多种不同性质的任务队列,如ArrayBlockingQueue(有界阻塞队列)、LinkedBlockingQueue(可选有界 / 无界阻塞队列)等。这些队列的不同特性会影响线程池的行为,并且与线程池的调整策略相互配合。例如,有界队列满时会触发线程池创建新线程或者执行饱和策略。
ScheduledThreadPoolExecutor 使用的DelayedWorkQueue是无界延迟队列,主要用于存储定时任务,任务按照延迟时间或周期进行排序。这种特殊的任务队列使得线程池的任务存储和执行机制与 ThreadPoolExecutor 有很大不同,不能简单地应用 ThreadPoolExecutor 的调整策略。
13.7 Executors 提供构造 ScheduledThreadPoolExecutor 的方法
//返回的ScheduledExecutorService接口实际上是由ScheduledThreadPoolExecutor实现的
//这个接口提供了用于调度任务的方法
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
// 创建一个核心线程数为2的ScheduledThreadPoolExecutor
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(2);
// 提交一个延迟3秒后执行的任务
scheduledExecutor.schedule(() -> {
System.out.println("任务执行了");
}, 3, TimeUnit.SECONDS);
// 记得关闭线程池
scheduledExecutor.shutdown();
}
}
13.8 ScheduledThreadPoolExecutor 中 scheduleAtFixedRate 和 scheduleWithFixedDelay 的区别
scheduleAtFixedRate
定义:该方法用于以固定频率执行任务。它会按照一个固定的周期(period)来安排任务的执行,从初始延迟(initialDelay)时间过后开始执行第一次任务,之后每次执行的时间间隔是固定的period。对任务执行频率要求严格,且任务执行时间相对稳定的场景。例如,定时采集系统性能指标,需要每隔固定时间(如 5 分钟)采集一次,无论上一次采集是否完成,都按照固定时间间隔执行下一次采集任务。
scheduleWithFixedDelay
定义:此方法用于以固定延迟执行任务。任务第一次执行是在初始延迟(initialDelay)之后,后续每次执行的延迟时间是固定的(delay),这个延迟时间是指前一次任务执行完成后到下一次任务开始执行之间的间隔。任务执行时间不固定,且需要在上一次任务执行完成后等待一段时间再执行下一次任务的场景。
14.线程池和消息队列区别?如果消息队列积压消息,怎么处理?
区别
线程池:主要用于管理和复用线程资源,以异步的方式执行任务。它将任务分配到线程中执行,目的是提高任务的执行效率,减少线程创建和销毁的开销,适用于处理 CPU 密集型或 I/O 密集型任务等多种任务类型。例如,在一个 Web 服务器中,使用线程池来处理用户的 HTTP 请求,通过合理配置线程池参数,可以高效地处理大量并发请求。
消息队列:是一种用于在不同组件或系统之间传递消息的数据结构和通信机制。它主要关注消息的存储、转发和顺序,确保消息能够可靠地从生产者传递到消费者,常用于解耦系统的不同模块、实现异步通信和流量削峰等场景。
消息队列处理消息堆积(这条与线程池无关)
处理消息队列积压消息的方法
增加消费者数量
原理:通过增加消息消费者的数量,可以加快消息的处理速度。例如,原本有一个消费者处理积压的消息,处理速度为每分钟 10 条消息,积压了 100 条消息需要 10 分钟才能处理完。如果增加到两个消费者,且每个消费者处理速度不变,那么处理完积压消息只需要 5 分钟。
注意事项:需要考虑消费者增加后对系统资源的消耗,如 CPU、内存、数据库连接等。同时,有些消息处理可能存在顺序依赖关系,增加消费者可能会打乱消息处理的顺序,需要确保系统在这种情况下仍然能够正确运行。例如,在处理订单消息时,如果订单的处理顺序对业务有影响(如先处理支付成功的订单,再处理退款订单),就需要谨慎考虑增加消费者可能带来的顺序问题。
优化消费者处理逻辑
原理:提高单个消费者处理消息的效率,从而加快积压消息的处理。这可能包括优化消息处理算法、减少不必要的操作(如数据库查询次数、网络请求次数)、采用更高效的编程语言或库等。例如,对消息处理中的数据库插入操作进行批量处理,而不是每条消息都进行一次插入操作,可以大大提高处理效率。
注意事项:在优化过程中需要进行充分的测试,确保优化后的处理逻辑不会引入新的问题,如消息丢失、消息处理错误等。同时,优化可能需要对消费者代码进行修改,需要考虑代码的可维护性和兼容性。
消息队列扩容
原理:如果消息队列是有界的,并且积压是因为队列容量不足导致的,可以考虑对消息队列进行扩容。这样可以容纳更多的消息,避免新消息因为队列满而丢失,同时也为处理积压消息争取更多的时间。例如,将消息队列的容量从 1000 条消息扩大到 2000 条消息。
注意事项:扩容可能涉及到系统架构的调整和资源的重新分配。对于分布式消息队列,还需要考虑数据迁移和一致性等问题。同时,扩容只是暂时缓解积压问题,还需要结合其他方法(如增加消费者数量或优化处理逻辑)来真正解决积压。
调整消息过期策略或丢弃策略
原理:对于一些时效性不强或者不太重要的消息,可以考虑调整消息的过期时间,让过期消息自动从队列中清除,或者设置合理的丢弃策略,主动丢弃一些不重要的消息。例如,在日志消息队列中,如果积压严重,可以丢弃一些低级别(如 DEBUG 级别)的日志消息,只保留重要的(如 ERROR 级别)消息。
注意事项:需要谨慎评估消息的重要性和丢弃消息可能带来的影响。对于一些关键业务消息,如金融交易消息、订单消息等,一般不建议轻易丢弃,否则可能会导致业务数据不一致或用户体验下降。
流量控制和消息限流
原理:在消息生产者端进行流量控制,减少新消息的产生速度,使消息产生速度与消费者处理速度相匹配。可以通过设置生产者的发送频率限制、采用令牌桶算法等方式来实现。例如,限制消息生产者每秒最多发送 10 条消息,这样可以避免消息队列进一步积压。
注意事项:流量控制需要考虑业务的实际需求,过度限制可能会影响业务的正常运行。例如,在一个实时数据采集系统中,如果过度限制数据采集的频率,可能会导致数据丢失或不及时,影响数据分析的准确性。