前言:线程池是 Java 中用于高效管理并发任务的工具,通过复用线程、降低线程创建销毁的开销,提升系统性能与响应速度。它帮助开发者更好地控制线程生命周期和资源消耗,是高并发应用的重要组成部分。
✨✨✨这里是秋刀鱼不做梦的BLOG
✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客
在正式开始讲解之前,先让我们看一下本文大致的讲解内容:
目录
1.线程池的核心原理
(1)基本概念
(2)线程池的工作流程
(3)线程池的关键组件的实现方式
1. 核心线程数与最大线程数
2. 任务队列
3. 线程工厂
4. 拒绝策略
2.线程池的使用
(1)线程池的参数介绍
(2)使用 Executors 创建常见的线程池
【1】newFixedThreadPool(int nThreads) - 固定线程数线程池
【2】newCachedThreadPool() - 可缓存线程池
【3】newSingleThreadExecutor() - 单线程线程池
【4】newScheduledThreadPool(int corePoolSize) - 定时/周期性线程池
3.为什么要使用线程池
(1)降低资源的消耗
(2)提高速度
(3)提高线程的可管理性
1.线程池的核心原理
(1)基本概念
在正式开始学习Java中的线程池之前,先让我们了解一下什么是线程池,以下为线程池的基本概念:
在并发编程中,线程池是一个重要的工具,它允许我们复用已经创建的线程,从而减少线程创建和销毁的开销。线程池可以有效地管理并发任务,避免系统因为创建过多线程而产生的性能瓶颈。
在了解完了线程池的基本概念之后,让我们看一下线程池的组成部分,线程池的基本结构通常由以下几个部分组成:
核心线程数 (corePoolSize
):线程池中常驻的线程数。即使没有任务执行,线程池也会保持这些线程在空闲状态。
最大线程数 (maximumPoolSize
):线程池中允许的最大线程数。如果任务数超过线程池的核心线程数并且任务队列已满,线程池将会创建更多的线程,直到达到最大线程数。
任务队列:用于存放提交到线程池的任务。不同类型的任务队列会影响线程池的性能。例如,LinkedBlockingQueue
是一个无界队列,可以存放大量任务,而 ArrayBlockingQueue
是一个有界队列,适合在任务数有上限的场景中使用。
线程工厂 (ThreadFactory
):用于创建新线程。你可以自定义线程工厂,以便为线程池中的线程命名或设置优先级等属性。
拒绝策略 (RejectedExecutionHandler
):当线程池无法接受新的任务时,可以采用不同的拒绝策略,比如丢弃任务或抛出异常。
读者在读完上述对线程池的组成部分的描述之后,可能还是不能理解线程池中的这些构成部分,不过没关系,随着我们对线程池的进一步讲解之后,读者就可以更好的理解这些部分了!
(2)线程池的工作流程
在了解完了线程池的基本概念之后,在让我们进一步了解一下Java中的线程池是如何工作的,其工作原理又是什么
线程池的基本工作流程可以分为以下几个步骤:
程可以分为以下几个步骤:
任务提交: 当一个任务被提交给线程池时,线程池首先会尝试将任务放入任务队列。如果任务队列有空间,任务就会排队等待执行。如果任务队列满了,且线程池中的线程数未达到最大线程数,则线程池会创建新的线程来处理任务。如果线程池中的线程数已经达到最大值,且任务队列也满了,线程池会根据配置的拒绝策略来处理任务。
任务执行: 线程池中的线程会从任务队列中获取任务并执行。执行完成后,线程并不会被销毁,而是返回到线程池中,准备接收下一个任务。
线程回收: 如果线程池中的线程长时间处于空闲状态,且空闲时间超过了设定的阈值,线程池会回收这些线程,以节省系统资源。回收的线程数不会低于核心线程数,只有当线程数大于核心线程数时,线程池才会销毁空闲线程。
拒绝策略触发: 如果线程池的任务队列已满,并且线程池中的线程数已经达到了最大线程数,再提交的任务就会被拒绝。这时,线程池会根据配置的拒绝策略来处理任务,如抛出异常、丢弃任务、丢弃队列中最老的任务,或者由提交任务的线程自己执行任务。
通过上述的讲解,我们就大致的了解了Java中的线程池是如何工作的了,这对于我们接下来的学习是至关重要的。
(3)线程池的关键组件的实现方式
在上文中我们已经了解了Java中的线程池的基本结构了,这里我们进一步讲解一下线程池的关键组件的实现方式。
线程池的组件包括核心线程数、最大线程数、任务队列、线程工厂和拒绝策略,这些组件的配置决定了线程池的行为与性能,下面是每个组件的详细介绍。
1. 核心线程数与最大线程数
核心线程数:线程池在没有任务时保持的最小线程数,避免了线程的频繁创建和销毁。通过合理设置 corePoolSize
,可以确保在任务负载较轻时,线程池仍然保持一定数量的线程,以便及时响应新的任务。
最大线程数:线程池允许的最大线程数。当任务量剧增且任务队列已满时,线程池会根据 maximumPoolSize
来创建新的线程,但不会超过该最大值。
2. 任务队列
线程池的任务队列有不同的实现方式:
LinkedBlockingQueue
:一个无界队列,适用于任务量较大的场景,能容纳大量待处理的任务。若任务队列中有空位,线程池就会继续添加任务,不会立即创建新的线程。
ArrayBlockingQueue
:一个有界队列,适用于任务量较小且固定的场景。当任务队列已满时,线程池会尝试创建新的线程,直到达到最大线程数。
SynchronousQueue
:一个零容量队列,适合任务较为紧凑并且执行迅速的场景。每当一个任务到来时,必须有线程立即接收并执行这个任务。
3. 线程工厂
线程工厂用于创建线程。通过自定义线程工厂,开发者可以为线程指定特定的名称、优先级,或者让线程成为守护线程等。
4. 拒绝策略
线程池中的拒绝策略用于处理任务过载时的情况。常见的拒绝策略包括:
AbortPolicy
:默认策略,抛出 RejectedExecutionException
异常。
DiscardPolicy
:丢弃当前任务。
DiscardOldestPolicy
:丢弃任务队列中最旧的任务。
CallerRunsPolicy
:让提交任务的线程来执行该任务。
至此,我们就大致的对Java中的线程池有了初步的理解了!!!
2.线程池的使用
在了解完了Java中的线程池的基本概念以及原理之后,现在让我们学习一下如何去使用Java中的线程池吧,不过首先我们需要先了解一下Java中的线程池的参数。
(1)线程池的参数介绍
Java 中使用 ThreadPoolExecutor
来创建自定义线程池。通过构造方法,可以传入多个参数来配置线程池,具体参数如下:
public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 线程空闲存活时间 TimeUnit unit, // 线程空闲存活时间的单位 BlockingQueue<Runnable> workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略);
参数解释:
corePoolSize(核心池大小)
定义:核心池大小是线程池中始终保持活动的线程数,即使线程池中的任务数较少,核心线程也会一直存在,除非设置了allowCoreThreadTimeOut
为 true
。作用:决定了线程池中最小的线程数量。这些线程会一直存活,直到线程池被关闭。调优建议:对于 CPU 密集型任务,可以设置为与 CPU 核心数相等;对于 IO 密集型任务,可以适当增大。 int corePoolSize = 5; // 核心线程数
maximumPoolSize(最大池大小)
定义:最大池大小是线程池中能够创建的最大线程数。当任务队列满了,且线程池中的线程数小于maximumPoolSize
时,线程池会创建新线程来处理任务。作用:限制线程池中最大的并发线程数。如果任务量非常大,且有大量任务需要处理,maximumPoolSize
设得较大可以避免任务的阻塞。调优建议:如果系统的硬件资源充足,且任务的数量和处理时间不确定,可以适当增加 maximumPoolSize
。 int maximumPoolSize = 10; // 最大线程数
keepAliveTime(线程存活时间)
定义:当线程池中的线程数超过核心线程数时,空闲线程的最大存活时间。超出这个时间,线程会被终止并从池中移除。作用:如果线程池中的线程多于核心线程数,但线程在一定时间内未被使用,那么这些线程会被回收。调优建议:对于任务量变化大的应用,可以适当调整keepAliveTime
,以节省资源。对于高并发任务,可以适当增加此值。 long keepAliveTime = 60L; // 线程存活时间,单位为秒
unit(时间单位)
定义:keepAliveTime
参数的时间单位,通常是 TimeUnit
类提供的常量,如 TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
、TimeUnit.MINUTES
等。作用:决定 keepAliveTime
使用的单位,方便开发者在设置时选择不同的时间粒度。调优建议:如果线程池中的线程空闲时间较短,可以选择秒作为时间单位;如果线程空闲时间较长,选择分钟等较大时间单位。 TimeUnit unit = TimeUnit.SECONDS; // 设置时间单位为秒
workQueue(任务队列)
定义:线程池中的任务队列,用于存储等待执行的任务。常见的任务队列有LinkedBlockingQueue
、ArrayBlockingQueue
、SynchronousQueue
等。作用:当线程池中的线程数量达到 corePoolSize
时,新提交的任务会被放入任务队列等待执行。任务队列的选择直接影响线程池的性能。调优建议:对于任务数量不确定的情况,可以选择无界队列(如 LinkedBlockingQueue
);如果需要限制队列的大小,则可以使用有界队列(如 ArrayBlockingQueue
)。 BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 设置任务队列容量为100
handler(拒绝策略)
定义:当线程池中的线程数达到maximumPoolSize
,且任务队列已满时,新的任务提交就会被拒绝。此时可以通过 RejectedExecutionHandler
处理任务的拒绝。作用:拒绝策略决定了当任务无法被线程池处理时的处理方式。常见的拒绝策略有: AbortPolicy
:直接抛出 RejectedExecutionException
,默认策略。CallerRunsPolicy
:由调用线程处理该任务,避免任务丢失。DiscardPolicy
:直接丢弃任务。DiscardOldestPolicy
:丢弃队列中最旧的任务。调优建议:如果任务丢失不可接受,推荐使用 CallerRunsPolicy
。如果可以容忍任务丢失,则可以选择 DiscardPolicy
。 RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 使用调用者运行策略
threadFactory(线程工厂)
定义:线程池使用线程工厂来创建新的线程。可以自定义线程工厂,以定制线程的创建过程(如设置线程的名称、优先级、是否为守护线程等)。作用:threadFactory
允许开发者控制线程的创建过程,特别是在需要对线程进行一些特殊配置(如设置线程名称、线程优先级、守护线程等)时非常有用。调优建议:通常情况下,使用默认的线程工厂就足够了。如果需要自定义线程行为,可以实现 ThreadFactory
接口。 ThreadFactory threadFactory = new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("CustomThread-" + thread.getId()); return thread; }};
通过上述对线程池中的参数的讲解,我们就大致的了解了Java中线程池该如果创建了,那么现在让我们使用一个案例将上述所讲的串联起来,以下为一个自定义线程池:
import java.util.concurrent.*;public class CustomThreadPool { public static void main(String[] args) { int corePoolSize = 5; // 核心线程数 int maximumPoolSize = 10; // 最大线程数 long keepAliveTime = 60L; // 线程存活时间 TimeUnit unit = TimeUnit.SECONDS; // 时间单位 BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 任务队列 RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 拒绝策略 ThreadFactory threadFactory = new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("CustomThread-" + thread.getId()); return thread; } }; ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler ); // 提交任务 for (int i = 0; i < 10; i++) { int taskId = i; executor.submit(() -> { System.out.println("执行任务 " + taskId + ",线程: " + Thread.currentThread().getName()); }); } executor.shutdown(); }}
在这个示例中,我们使用了自定义的线程池参数来创建一个 ThreadPoolExecutor
,并提交了任务来执行。通过配置这些参数,我们能够更好地控制线程池的行为,确保系统在高并发条件下高效运行。
(2)使用 Executors
创建常见的线程池
在Java中除了ThreadPoolExecutor之外,Executors
工厂类也为我们提供了几种常用的线程池创建方法,下面是几种常见线程池的创建和使用方法
【1】newFixedThreadPool(int nThreads)
- 固定线程数线程池
这种线程池创建一个固定大小的线程池,线程池中的线程数在创建后保持不变。当所有线程都处于工作状态时,新任务将进入等待队列中,直到有线程空闲出来。
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class FixedThreadPoolExample { public static void main(String[] args) { // 创建一个固定线程数量为 3 的线程池 ExecutorService executor = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { executor.execute(() -> { System.out.println(Thread.currentThread().getName() + " 正在执行任务"); }); } // 关闭线程池 executor.shutdown(); }}
这种方式创建线程池的特点:
适用于执行长期任务,性能稳定。线程池中的线程数固定,不会变化。如果所有线程都在工作,新的任务会被放入等待队列中,等待有空闲线程时执行。
【2】newCachedThreadPool()
- 可缓存线程池
这种线程池会根据任务需要创建新线程,并复用先前构建的线程。池中的线程如果在 60 秒内都没有被使用,则会被终止并从池中移除。
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class CachedThreadPoolExample { public static void main(String[] args) { // 创建一个可缓存的线程池 ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { executor.execute(() -> { System.out.println(Thread.currentThread().getName() + " 正在执行任务"); }); } // 关闭线程池 executor.shutdown(); }}
这种方式创建线程池的特点:
适用于执行大量短期任务。当线程空闲 60 秒后自动回收,避免资源浪费。线程池大小不固定,按需动态分配。
【3】newSingleThreadExecutor()
- 单线程线程池
这种线程池始终使用唯一的工作线程来执行任务,所有任务按提交顺序执行。如果该线程异常终止,一个新线程会取而代之,继续执行后续任务。
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class SingleThreadExecutorExample { public static void main(String[] args) { // 创建一个单线程化的线程池 ExecutorService executor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 5; i++) { executor.execute(() -> { System.out.println(Thread.currentThread().getName() + " 正在执行任务"); }); } // 关闭线程池 executor.shutdown(); }}
这种方式创建线程池的特点:
适用于需要保证任务顺序执行的场景。只有一个线程工作,所有任务会按顺序执行。可确保任务按提交顺序执行。
【4】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) { // 创建一个支持定时及周期性任务的线程池 ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); // 延迟 2 秒后执行任务 executor.schedule(() -> { System.out.println("延迟 2 秒后执行任务"); }, 2, TimeUnit.SECONDS); // 延迟 1 秒后开始执行任务,之后每 3 秒执行一次 executor.scheduleAtFixedRate(() -> { System.out.println("每 3 秒执行一次任务"); }, 1, 3, TimeUnit.SECONDS); // 关闭线程池 executor.shutdown(); }}
这种方式创建线程池的特点:
适用于需要周期性执行任务的场景。可以指定延迟执行,也可以按照固定的时间间隔循环执行。 以上就是使用Java中使用Executors
工厂类来创建线程池的方式了!至此我们就了解了Java中该如何创建并使用线程池了。
3.为什么要使用线程池
在了解完了如何在Java中使用线程池之后,可能读者就会发问了,我们为什么要使用线程池呢?线程池有什么优点呢?那么我们这就解释一下为什么使用线程池。
(1)降低资源的消耗
线程池通过复用线程池中的线程,避免了频繁的线程创建和销毁,从而降低了资源消耗。每次创建线程的成本较高,尤其是在并发量大的场景中,频繁地创建和销毁线程会导致系统性能下降。线程池通过维持一定数量的线程,复用这些线程处理任务,减少了频繁创建线程的开销。
示例:
public class ThreadCreationTest { public static void main(String[] args) { long startTime = System.nanoTime(); for (int i = 0; i < 1000; i++) { new Thread(() -> { // 模拟一些简单的计算任务 for (int j = 0; j < 1000; j++) { Math.sqrt(j); } }).start(); } long endTime = System.nanoTime(); System.out.println("线程创建和销毁的时间: " + (endTime - startTime) + " 纳秒"); // 使用线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); startTime = System.nanoTime(); for (int i = 0; i < 1000; i++) { executorService.submit(() -> { // 模拟一些简单的计算任务 for (int j = 0; j < 1000; j++) { Math.sqrt(j); } }); } executorService.shutdown(); endTime = System.nanoTime(); System.out.println("线程池的时间: " + (endTime - startTime) + " 纳秒"); }}
上面的代码模拟了两种方式:直接创建线程和使用线程池处理任务。通过对比这两者的时间消耗,我们就可以看到线程池显著减少了线程创建和销毁的开销。
(2)提高速度
线程池通过预先创建一定数量的线程,可以在任务到来时迅速响应,当任务提交到线程池时,如果有空闲线程,线程池可以立即开始执行任务,避免了任务排队等待线程创建的时间,确保任务尽可能快地被处理。
示例:
public class RequestHandler { private static final int THREAD_POOL_SIZE = 10; private static final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); public static void handleRequest(int requestId) { executorService.submit(() -> { try { System.out.println("处理请求 " + requestId + " 的线程:" + Thread.currentThread().getName()); Thread.sleep(200); // 模拟处理请求的时间 } catch (InterruptedException e) { e.printStackTrace(); } }); } public static void main(String[] args) { for (int i = 0; i < 50; i++) { handleRequest(i); } executorService.shutdown(); }}
在这个例子中,我们模拟了 50 个请求的并发处理。线程池使用了 10 个线程来并发处理请求,并且任务的执行时间(Thread.sleep(200)
)模拟了请求的处理过程。通过线程池,多个请求可以同时被多个线程处理,而不需要等待线程的创建。
(3)提高线程的可管理性
线程池不仅能有效地复用线程,还提供了线程的生命周期管理。通过合理设置线程池的参数,开发者可以控制线程的创建、销毁以及空闲时的回收方式,从而确保系统在负载较重时仍能稳定运行。
示例:
import java.util.concurrent.*;public class DynamicThreadPoolTest { private static final int CORE_POOL_SIZE = 5; private static final int MAX_POOL_SIZE = 10; private static final int QUEUE_CAPACITY = 20; private static ThreadPoolExecutor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(QUEUE_CAPACITY) ); public static void handleTask(int taskId) { executor.submit(() -> { try { System.out.println("任务 " + taskId + " 正在执行,线程: " + Thread.currentThread().getName()); Thread.sleep(200); // 模拟任务执行时间 } catch (InterruptedException e) { e.printStackTrace(); } }); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { handleTask(i); } // 动态调整线程池的大小 executor.setCorePoolSize(7); executor.setMaximumPoolSize(15); System.out.println("线程池已调整为新的配置"); // 关闭线程池 executor.shutdown(); }}
在上述代码中,线程池最初使用了 5 个核心线程和 10 个最大线程,但在任务提交过程中,我们根据负载情况动态调整了线程池的大小(通过调用 setCorePoolSize
和 setMaximumPoolSize
)。这种动态调整可以有效应对负载变化,从而优化线程池的性能。
这样我们就理解了,为什么要使用线程池了!!!
以上就是本篇文章的全部内容了~~~