当前位置:首页 » 《休闲阅读》 » 正文

Java中的线程池(如果想知道Java中有关线程池的知识,那么只看这一篇就足够了!)

24 人参与  2024年11月20日 12:02  分类 : 《休闲阅读》  评论

点击全文阅读


        前言:线程池是 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(核心池大小)

定义:核心池大小是线程池中始终保持活动的线程数,即使线程池中的任务数较少,核心线程也会一直存在,除非设置了 allowCoreThreadTimeOuttrue作用:决定了线程池中最小的线程数量。这些线程会一直存活,直到线程池被关闭。调优建议:对于 CPU 密集型任务,可以设置为与 CPU 核心数相等;对于 IO 密集型任务,可以适当增大。
int corePoolSize = 5; // 核心线程数

maximumPoolSize(最大池大小)

定义:最大池大小是线程池中能够创建的最大线程数。当任务队列满了,且线程池中的线程数小于 maximumPoolSize 时,线程池会创建新线程来处理任务。作用:限制线程池中最大的并发线程数。如果任务量非常大,且有大量任务需要处理,maximumPoolSize 设得较大可以避免任务的阻塞。调优建议:如果系统的硬件资源充足,且任务的数量和处理时间不确定,可以适当增加 maximumPoolSize
int maximumPoolSize = 10; // 最大线程数

keepAliveTime(线程存活时间)

定义:当线程池中的线程数超过核心线程数时,空闲线程的最大存活时间。超出这个时间,线程会被终止并从池中移除。作用:如果线程池中的线程多于核心线程数,但线程在一定时间内未被使用,那么这些线程会被回收。调优建议:对于任务量变化大的应用,可以适当调整 keepAliveTime,以节省资源。对于高并发任务,可以适当增加此值。
long keepAliveTime = 60L; // 线程存活时间,单位为秒

unit(时间单位)

定义keepAliveTime 参数的时间单位,通常是 TimeUnit 类提供的常量,如 TimeUnit.SECONDSTimeUnit.MILLISECONDSTimeUnit.MINUTES 等。作用:决定 keepAliveTime 使用的单位,方便开发者在设置时选择不同的时间粒度。调优建议:如果线程池中的线程空闲时间较短,可以选择秒作为时间单位;如果线程空闲时间较长,选择分钟等较大时间单位。
TimeUnit unit = TimeUnit.SECONDS; // 设置时间单位为秒

workQueue(任务队列)

定义:线程池中的任务队列,用于存储等待执行的任务。常见的任务队列有 LinkedBlockingQueueArrayBlockingQueueSynchronousQueue 等。作用:当线程池中的线程数量达到 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 个最大线程,但在任务提交过程中,我们根据负载情况动态调整了线程池的大小(通过调用 setCorePoolSizesetMaximumPoolSize)。这种动态调整可以有效应对负载变化,从而优化线程池的性能。

        这样我们就理解了,为什么要使用线程池了!!!


以上就是本篇文章的全部内容了~~~


点击全文阅读


本文链接:http://zhangshiyu.com/post/189184.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1