在 Java 中,池化结构(Pooling Structure)是一种常用的设计模式,用于管理和重复使用有限的资源。这种结构通过预先创建一定数量的资源对象(如线程、数据库连接、对象等),然后将这些对象集中管理并分配给请求者使用。在使用完毕后,这些资源对象不会被销毁,而是返回到池中,供下一个请求者再次使用。
池化结构在 Java 中广泛应用于各类需要频繁创建和销毁资源的场景中。通过重复使用资源对象,池化结构能够有效提高资源的使用率、控制资源的数量,并提升系统的性能和稳定性。在开发中,合理设计和配置池化结构,对系统的高效运行至关重要。
池化结构的作用
-
提高资源使用率:
-
池化结构通过重复使用资源对象,避免了频繁的资源创建和销毁操作。这不仅减少了资源分配和释放的开销,还提高了资源的使用效率。例如,通过线程池可以复用线程,从而避免了频繁创建和销毁线程带来的性能损耗。
-
-
控制资源的数量:
-
池化结构通常允许设置资源对象的最大数量,从而避免系统中出现过多的资源实例,导致资源枯竭或性能下降。例如,数据库连接池通过限制连接的最大数量,防止数据库因过多连接而崩溃。
-
-
提高系统性能和稳定性:
-
由于资源的复用,池化结构能够显著减少资源分配的时间,从而提高系统的响应速度。此外,池化结构还可以通过合理配置池的大小和参数,防止系统资源被过度消耗,提高系统的稳定性。
-
-
简化资源管理:
-
池化结构通常包含资源的管理逻辑,如资源的初始化、复用、回收、超时处理等。通过统一的管理方式,池化结构可以简化资源的管理工作,减少开发人员的负担。
-
线程池(Thread Pool)
线程池(Thread Pool)是 Java 中一种非常重要的多线程并发编程机制。通过使用线程池,你可以更有效地管理和重用线程资源,而不是每次执行任务时都创建和销毁线程。
线程池的工作原理
线程池的基本思想是:预先创建一定数量的线程,这些线程处于待命状态,等待执行任务。当有任务提交到线程池时,线程池将任务分配给空闲的线程去执行。如果所有线程都在执行任务,线程池可以根据配置采取不同的策略,比如等待、拒绝任务或创建新的线程等。
线程池的主要优势
-
减少资源开销:创建和销毁线程是有代价的,特别是在高并发环境下。通过线程池,可以重用已创建的线程,减少资源开销。
-
提高响应速度:任务到达时无需等待线程创建,可以直接执行任务。
-
方便管理:线程池可以统一管理线程,设置线程的最大并发数量,避免系统因线程过多而资源耗尽。
Java 中的线程池实现
Java 中 java.util.concurrent
包提供了 Executor
框架,方便地创建和管理线程池。常用的线程池类有:
1. 固定大小线程池 (FixedThreadPool
)
Executors.newFixedThreadPool(int nThreads)
创建一个固定大小的线程池,线程池中的线程数量始终不变。
用途:
-
固定大小线程池创建一个包含固定数量线程的线程池。这些线程在整个应用程序的生命周期中会一直存在,直到线程池被显式关闭。
特点:
-
在这个线程池中,线程数是固定的,即使有大量任务需要执行,线程池也只会使用固定数量的线程来处理任务。当所有线程都在忙碌时,其他任务会进入队列等待执行。
适用场景:
-
适用于需要控制并发任务数量的场景,避免系统因资源消耗过多而变得不稳定。例如,服务器应用中控制同时处理的请求数量。
示例代码:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class FixedThreadPoolExample { public static void main(String[] args) { // 创建一个固定大小为3的线程池 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); // 提交5个任务到线程池 for (int i = 1; i <= 5; i++) { final int taskId = i; fixedThreadPool.execute(() -> { System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName()); try { Thread.sleep(2000); // 模拟任务执行时间 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " completed."); }); } // 关闭线程池 fixedThreadPool.shutdown(); } }
作用:
-
适用于有固定数量线程的场景,限制并发线程的数量。
-
常用于需要控制并发任务数量的应用场景。
终端输出:
Task 1 is running on thread pool-1-thread-1 Task 2 is running on thread pool-1-thread-2 Task 3 is running on thread pool-1-thread-3 Task 1 completed. Task 2 completed. Task 3 completed. Task 4 is running on thread pool-1-thread-1 Task 5 is running on thread pool-1-thread-2 Task 4 completed. Task 5 completed.
说明:线程池中有3个线程,先执行前3个任务,前3个任务完成后,线程池继续执行后续的任务。
2. 缓存线程池 (CachedThreadPool
)
Executors.newCachedThreadPool()
创建一个可缓存的线程池,线程池大小不固定,可以根据需求动态创建新线程。适用于大量短生命周期的任务。
用途:
-
缓存线程池是一个大小不固定的线程池,它可以根据需要动态创建新的线程。当有新的任务到达时,如果有空闲线程则使用空闲线程执行任务,如果没有则创建新线程。
特点:
-
线程池中线程数几乎不受限制,通常适合处理大量短期的小任务。缓存线程池在执行完任务后会回收线程并将其存入池中,当池中线程空闲超过一定时间(通常是60秒)后,线程会被终止并从池中移除。
适用场景:
-
适用于负载波动较大、任务执行时间短的场景,例如处理并发数量不确定的即时任务请求。它能有效地应对瞬时的大量请求。
示例代码:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class CachedThreadPoolExample { public static void main(String[] args) { // 创建一个缓存线程池 ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 提交5个任务到线程池 for (int i = 1; i <= 5; i++) { final int taskId = i; cachedThreadPool.execute(() -> { System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName()); try { Thread.sleep(2000); // 模拟任务执行时间 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " completed."); }); } // 关闭线程池 cachedThreadPool.shutdown(); } }
作用:
-
创建一个可根据需要动态增加线程数量的线程池。
-
适用于执行大量短期任务或负载变化较大的场景。
终端输出:
Task 1 is running on thread pool-1-thread-1 Task 2 is running on thread pool-1-thread-2 Task 3 is running on thread pool-1-thread-3 Task 4 is running on thread pool-1-thread-4 Task 5 is running on thread pool-1-thread-5 Task 1 completed. Task 2 completed. Task 3 completed. Task 4 completed. Task 5 completed.
说明:由于缓存线程池会根据需要创建新线程,因此所有任务几乎是同时启动并执行的
3. 单线程池 (SingleThreadExecutor
)
Executors.newSingleThreadExecutor()
创建一个单线程的线程池,这个线程池中只有一个线程在工作,所有任务被保证按顺序执行。
用途:
-
单线程池是一个只有一个工作线程的线程池。这个线程池保证所有任务按提交顺序逐一执行,在同一时刻最多只有一个任务在执行。
特点:
-
这个线程池确保所有任务按照顺序执行,任务之间不会有并发问题。即使线程因某种原因意外终止,也会有一个新的线程替代它继续执行后续任务。
适用场景:
-
适用于需要保证任务顺序执行的场景,例如需要顺序写入文件或顺序处理网络请求等应用场景。
示例代码:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SingleThreadExecutorExample { public static void main(String[] args) { // 创建一个单线程的线程池 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); // 提交5个任务到线程池 for (int i = 1; i <= 5; i++) { final int taskId = i; singleThreadExecutor.execute(() -> { System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName()); try { Thread.sleep(2000); // 模拟任务执行时间 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " completed."); }); } // 关闭线程池 singleThreadExecutor.shutdown(); } }
作用:
-
用于保证任务按顺序执行的场景,适用于不需要并发、但要顺序执行任务的应用。
终端输出:
Task 1 is running on thread pool-1-thread-1 Task 1 completed. Task 2 is running on thread pool-1-thread-1 Task 2 completed. Task 3 is running on thread pool-1-thread-1 Task 3 completed. Task 4 is running on thread pool-1-thread-1 Task 4 completed. Task 5 is running on thread pool-1-thread-1 Task 5 completed.
说明:单线程池中只有一个线程,因此任务是按顺序依次执行的。
4. 定时线程池 (ScheduledThreadPool
)
Executors.newScheduledThreadPool(int corePoolSize)
创建一个定时执行或周期性执行任务的线程池。
用途:
-
定时线程池用于定时执行任务或周期性地执行任务。它允许任务在指定延迟后执行或以固定的时间间隔重复执行。
特点:
-
定时线程池在延迟或周期性任务处理方面非常有效。它在初始化时创建固定数量的线程,这些线程可用于处理定时或周期任务。它能很好地支持基于时间的任务调度,如定时报告生成、定期清理任务等。
适用场景:
-
适用于需要定期或定时执行任务的场景,例如定时备份、定期发送邮件通知、或定时清理过期数据等。
示例代码:
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ScheduledThreadPoolExample { public static void main(String[] args) { // 创建一个定时线程池,核心线程数为2 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2); // 提交定时任务,延迟3秒执行 scheduledThreadPool.schedule(() -> { System.out.println("Task 1 is running on thread " + Thread.currentThread().getName()); }, 3, TimeUnit.SECONDS); // 提交周期性任务,延迟1秒后每2秒执行一次 scheduledThreadPool.scheduleAtFixedRate(() -> { System.out.println("Task 2 is running on thread " + Thread.currentThread().getName()); }, 1, 2, TimeUnit.SECONDS); // 10秒后关闭线程池 scheduledThreadPool.schedule(() -> scheduledThreadPool.shutdown(), 10, TimeUnit.SECONDS); } }
作用:
-
适用于需要延时执行或周期性执行任务的场景,如定时任务调度。
终端输出:
Task 2 is running on thread pool-1-thread-1 Task 1 is running on thread pool-1-thread-2 Task 2 is running on thread pool-1-thread-1 Task 2 is running on thread pool-1-thread-1 Task 2 is running on thread pool-1-thread-1
说明:第一个任务延迟3秒执行,第二个任务延迟1秒后每2秒执行一次,持续到线程池关闭。
5. 自定义线程池 (ThreadPoolExecutor
)
用途:
-
自定义线程池提供了对线程池参数的完全控制。开发者可以根据应用的具体需求定制线程池的行为,包括核心线程数、最大线程数、线程空闲时间、任务队列类型等。
特点:
-
自定义线程池是最灵活的线程池实现,允许开发者设置线程池的所有参数。例如,可以定义核心线程数以处理常见负载,设置最大线程数以应对突发流量,并配置任务队列来处理排队任务。它还可以配置线程的存活时间,以便在任务完成后适时释放资源。
适用场景:
-
适用于复杂应用场景下的精细化资源管理。例如,在一个高性能服务器中,可能需要精确控制线程的创建、销毁以及任务调度策略,从而优化资源利用率并保证系统稳定性。
示例代码:
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class CustomThreadPoolExample { public static void main(String[] args) { // 创建一个自定义的线程池,核心线程数为2,最大线程数为4,队列大小为2 ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor( 2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)); // 提交6个任务到线程池 for (int i = 1; i <= 6; i++) { final int taskId = i; customThreadPool.execute(() -> { System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName()); try { Thread.sleep(2000); // 模拟任务执行时间 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " completed."); }); } // 关闭线程池 customThreadPool.shutdown(); } }
作用:
-
提供了完全的配置灵活性,允许根据应用需求自定义线程池的参数。
终端输出:
Task 1 is running on thread pool-1-thread-1 Task 2 is running on thread pool-1-thread-2 Task 3 is running on thread pool-1-thread-3 Task 4 is running on thread pool-1-thread-4 Task 1 completed. Task 5 is running on thread pool-1-thread-1 Task 2 completed. Task 6 is running on thread pool-1-thread-2 Task 3 completed. Task 4 completed. Task 5 completed. Task 6 completed.
说明:前两个任务使用核心线程执行,任务3和任务4进入队列,后续两个任务启动新的线程执行。
线程池的核心参数
-
Java线程池提供了一些常见的参数,用于配置线程池的行为和性能
-
corePoolSize(核心池大小):标识线程池中核心线程的数量,核心线程会一致存活,即使没有任务需要执行,默认情况下,核心池的大小为0
-
maximumPoolSize(最大池大小):标识线程池中最大线程的数量,当工作队列已满并且池中的线程数小于最大池大小的时候,线程池会创建新的线程来处理任务,直到达到最大池的大小。默认情况下,最大池大小为Inter.MAX_VALUE。
-
keepAliveTime(线程空闲时间):表示当线程池中的线程数量超过核心吃大小的时候,空闲线程的存活时间,当前程空闲时间超过keepAlive时,多余的线程会被销毁,直到线程池中的线程数等于核心池大小。默认情况下,空闲线程会被立即销毁
-
unit(事件单位):用于指定keepAliveTime的事件单位,可以是纳秒(TimeUnit.NANOSECONDS)、微秒(TimeUnit.MICROSECONDS)、亳秒(TimeUnit.MILLISECONDS)、秒(TimeUnit.SECONDS)、分钟(TimeUnit.MINUTES)、小时(TimeUnit.HOURS)、天(TimeUnit.DAYS)
-
workQueue(工作队列):用于存储等待执行的任务。Java提供了多种类型的工作队列,如SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue等。默认情况下,使用无界队列LinkedBlockingQueue
-
threadFactory(线程工厂):用于创建新线程,可以自定义线程工厂来指定线程的命名规则,优先级等
-
handler(拒绝策略):用于处理线程池队列已满且无法接收新任务时候的情况。java提供了几种内置的拒绝策略,如AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。也,也可以自定义
-
线程池的生命周期管理
线程池具有明确的生命周期管理方法,可以在不同状态下对其进行操作:
-
shutdown()
:启动线程池的有序关闭流程,已提交的任务会继续执行,但不再接受新任务。 -
shutdownNow()
:尝试停止所有正在执行的任务,并返回等待执行的任务列表。 -
awaitTermination()
:在关闭线程池后,等待所有任务完成或超时。
管理好线程池的生命周期可以防止资源泄漏,确保系统在关闭时能够正确地释放资源。
线程池的拒绝策略
当线程池中的任务数量达到最大值,并且任务队列已满时,线程池需要决定如何处理新的任务。Java 提供了四种默认的拒绝策略:
-
AbortPolicy
(默认):直接抛出RejectedExecutionException
异常。 -
CallerRunsPolicy
:由提交任务的线程执行该任务。 -
DiscardPolicy
:直接丢弃任务,不给予任何处理。 -
DiscardOldestPolicy
:丢弃队列中最早的任务,然后尝试重新提交当前任务。
线程池化的阻塞队列
线程池化的阻塞队列(Thread Pool with Blocking Queue)是线程池与阻塞队列的结合,常用于生产者-消费者模式中。阻塞队列用于存储等待执行的任务,而线程池中的线程则不断从队列中取出任务执行。
-
ArrayBlockingQueue
:数组阻塞队列-
任务存储在一个固定大小的数组中。
-
ArrayBlockingQueue
:-
特性
-
基于数组的有界阻塞队列,队列长度固定,支持公平和非公平锁机制。
-
一旦队列达到最大容量,生产者线程将被阻塞,直到有任务被消费。
-
-
适用场景
-
适合于任务数量有明确上限的场景,防止因过多任务堆积导致内存溢出。
-
-
-
-
LinkedBlockingQueue
:链表阻塞队列-
任务以链表的形式存储,可以动态扩展。
-
特性:
-
基于链表的阻塞队列,默认是无界的,但可以设置上限。
-
因为没有固定大小的数组限制,所以任务存储能力更强,且吞吐量较高。
适用场景:
-
适合任务流量较大且不希望丢失任务的场景,可以提供更高的并发吞吐量。
-
-
-
SynchronousQueue
:同步队列-
任务直接从生产者传递到消费者,不会存储任务。
-
特性:
-
没有任何内部容量的阻塞队列,不能存储任务,生产者线程提交的任务必须直接交给消费者线程处理。
-
如果没有空闲线程来接收任务,生产者线程会被阻塞。
适用场景:
-
适合每个任务必须立即处理的场景,或在需要通过增加线程来应对高负载的情况下使用。
-
-
-
PriorityBlockingQueue
:优先级阻塞队列-
任务根据优先级排序,优先级高的任务先处理。
-
特性:
-
一个无界的优先级队列,任务根据优先级而不是插入顺序进行排序。
-
当有多个任务时,高优先级的任务会被优先处理。
适用场景:
-
适合需要按优先级处理任务的场景,例如调度系统或需要按重要性排序的任务处理。
-
-
-
DelayQueue
:延迟队列-
任务需要等待一段时间后才能执行,常用于定时任务。
-
特性:
-
基于优先级队列的特殊队列,任务只有在其延迟时间到达之后才能被取出执行。
适用场景:
-
适合需要延迟执行任务的场景,例如定时任务调度、延迟消息队列等。
-
-
线程池中的阻塞队列选择
-
ArrayBlockingQueue
vsLinkedBlockingQueue
:-
ArrayBlockingQueue
适合固定任务数量且需要限制资源使用的情况; -
LinkedBlockingQueue
则更适合任务数量不确定且需要高并发处理的情况。
-
-
SynchronousQueue
的使用通常会导致线程池以最大并发来执行任务,因为它不存储任务,这在需要快速处理大量短任务时非常有效。 -
PriorityBlockingQueue
和DelayQueue
则更为特殊,适用于需要优先级调度和定时执行的任务。
选择合适的阻塞队列可以优化线程池的性能,保证系统的高效稳定运行。开发者应根据具体的任务需求、系统负载特点以及处理任务的紧急性来选择合适的阻塞队列。