【JUC基础】12. 线程池(一)

简介: 我们知道多线程的使用,是为了最大限度发挥现代多核处理器的计算能力,提高系统的吞吐量和性能。但是如果不加以控制和管理,随意使用多线程,对系统性能反而会有不利的影响。线程数量和系统CPU资源是息息相关的,随意使用甚至可能会耗尽系统CPU资源和内存资源。

1、前言

我们知道多线程的使用,是为了最大限度发挥现代多核处理器的计算能力,提高系统的吞吐量和性能。但是如果不加以控制和管理,随意使用多线程,对系统性能反而会有不利的影响。线程数量和系统CPU资源是息息相关的,随意使用甚至可能会耗尽系统CPU资源和内存资源。

2、池化技术

为了应对多线程管理和控制的问题,引入池化技术。所谓池化技术,顾名思义就是造一个池子,讲需要管理的东西交给池子管理,而用完之后就放回池子。就像小孩子的玩具收纳箱,玩玩具的时候从收纳箱中拿出玩具,玩完之后一定要教小孩子将玩具放回玩具收纳箱中。池化技术通过优化资源的分配效率,从来达到性能的调优。

image.png

其实在Java编程中,池化技术不仅仅在多线程中使用这种方式,其他地方也同样用到了池化技术。如:数据连接池,对象池,内存池等等。

3、为什么要用线程池

前面基础多少讲到了为什么使用线程池的原因。这里详细说明以下几个原因,由ChatGPT来总结一下:

在多线程编程中,频繁地创建和销毁线程是一项昂贵的操作。因此,使用线程池来管理线程的创建、复用和销毁是一种有效的方式。 以下是几个原因解释为什么要使用线程池:

    1. 降低资源消耗:线程的创建和销毁需要消耗系统资源,如内存和CPU。使用线程池可以重用线程,避免频繁创建和销毁线程,从而降低了资源消耗。
    2. 提高系统响应性:线程池能够提高系统的并发能力和响应性。通过合理地配置线程池的大小,可以同时执行多个任务,提高系统的吞吐量和响应时间。
    3. 任务调度和线程复用:线程池可以管理和调度任务的执行。它维护一组线程,可以根据任务的到达顺序和优先级来选择合适的线程执行任务,避免任务争抢和冲突。同时,线程池中的线程可以被重复利用来执行多个任务,避免了频繁创建线程的开销。
    4. 控制并发线程数量:通过设置线程池的大小和任务队列的容量,可以限制并发执行的线程数量,防止系统资源被过度占用,从而提高系统的稳定性和可靠性。
    5. 简化线程编程:使用线程池可以将任务的提交和执行解耦,简化了线程编程的复杂性。开发人员只需关注任务的实现和提交,无需手动创建和管理线程,从而降低了出错的概率。

    3.1、线程池优点

    线程池优点很明显,上面提到为什么要使用线程池的几个原因就是对应的优点,这里不赘述。

    3.2、线程池缺点

    线程池的缺点也很明显:

      1. 资源占用:线程池本身会占用一定的系统资源,包括内存和CPU。如果线程池的大小设置不合理,可能会导致资源浪费或不足的问题。
      2. 线程泄露:如果没有正确地关闭线程池,或者任务执行过程中出现异常导致线程无法正常释放,可能会导致线程泄露,进而影响系统性能。
      3. 需要合理配置:线程池的性能和效果受到配置参数的影响,需要根据具体场景合理配置线程池的大小、任务队列的容量等参数,否则可能会影响系统的性能和响应性。
      4. 难以处理长时间任务:线程池主要适用于短时间的任务处理,如果任务执行时间过长,可能会导致线程池中的线程被长时间占用,影响其他任务的执行。

      4、如何使用线程池

      最简单的线程池使用方法:

      public class ThreadPoolTest {
          public static void main(String[] args) {
              // 创建一个固定大小的线程池,大小为3
              ExecutorService executorService = Executors.newFixedThreadPool(3);
              // 提交任务给线程池执行
              for (int i = 0; i < 10; i++) {
                  // 执行提交任务
                  executorService.execute(() -> {
                      // ......
                  });
              }
              // 关闭线程池
              executorService.shutdown();
          }
      }

      image.gif

        1. 通过Executors.newFixedThreadPool(3)方法创建了一个线程池,该线程池固定线程数量为3;
        2. 使用executorService.execute()方法执行向线程池内提交的线程任务;
        3. 执行完后,通过executorService.shutdown();关闭线程池资源;

        通过简单的线程池使用方式,我们就完成了基本的线程池操作。线程池会自动管理线程的创建和销毁,以及任务的调度和执行,帮我们简化了多线程编程的复杂性。

        5、JUC线程池

        image.png

        5.1、Executor

        Executor 线程池顶级接口,类似一个线程池工厂。接口中只有一个execute()方法,接收Runnable类型。注意这里返回值类型是void。

        image.png

        5.2、ExecutorService

        ExecutorService继承自Executor接口,添加了关闭线程池以及等待中断等方法。同时添加了submit来提交线程任务,除了接收Runnable以外,还可以接收Callable类型,也增加了返回值。

        image.png

        5.3、AbstractExecutorService

        AbstractExecutorService是实现ExecutorService接口的抽象类。默认实现了个别如submit方法等。

        image.png

        5.4、ScheduledExecutorService

        该类是为了实现带有定时器功能的线程池。ScheduledExecutorService也是一个接口。包含了定时和延迟处理的方法。

        image.png

        5.5、ThreadPoolExecutor方法参数

        ThreadPoolExecutor重点看这个类。ThreadPoolExecutor是JUC中提供的默认线程池实现类。提供了丰富的配置选项和线程池管理功能。

        提供了4个可选配置的构造函数:

        public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, 
                                 long keepAliveTime, TimeUnit unit, 
                                 BlockingQueue<Runnable> workQueue)

        image.gif

        public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, 
                                  long keepAliveTime, TimeUnit unit, 
                                  BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)

        image.gif

        public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                                  long keepAliveTime, TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)

        image.gif

        public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                                  long keepAliveTime, TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

        image.gif

        我们重点关注其中的几个参数:corePoolSize,maximumPoolSize,keepAliveTime,workQueue,threadFactory,handler。

        5.5.1、corePoolSize

        核心线程数。指线程池中始终保持的线程数量,就算他们处于空闲状态,也不会被销毁。而一直存活的最小线程数量。

        5.5.2、maximumPoolSize

        最大线程数。指线程池中允许的最大线程数量。当maximumPoolSize的数量大于corePoolSize时,多的那部分空闲状态下的线程,会再超过一定时间后被销毁,只保留corePoolSize的核心线程数。

        5.5.3、keepAliveTime

        非核心线程保持空闲的时间。如果超过这个时间,多的那部分空闲状态下的线程就会被销毁。可以通过unit设置时间单位。

        5.5.4、BlockingQueue

        任务队列,阻塞队列。当前并发执行的线程数与系统资源有关。当你设置了大于当前系统可负载的线程数量时,多的那部分自然要进行等待,从而进入等待队列。

        当线程池中线程数量达到corePoolSize时,且都处于运行状态,这时候后续提交的线程任务会进入到缓存阻塞队列中,等待执行。这个缓存阻塞队列也就是workQueue。

        JUC中提供的BlockingQueue有以下几种:

          1. ArrayBlockingQueue:由数组实现的有界阻塞队列。需要指定队列的容量大小。当队列已满时,添加任务的操作将被阻塞,直到队列中有空位。ArrayBlockingQueue适用于固定大小的线程池,可以控制线程池中的最大任务数。
          2. LinkedBlockingQueue:由链表实现的可选有界或无界阻塞队列。如果创建LinkedBlockingQueue时没有指定容量大小,那么它将是一个无界队列,可以无限制地添加任务。如果指定了容量大小,它将成为一个有界队列。当队列已满时,添加任务的操作将被阻塞。LinkedBlockingQueue适用于任务数比较大且变化较大的场景。
          3. SynchronousQueue:一个没有缓冲区的阻塞队列。每个插入操作必须等待一个相应的删除操作,反之亦然。SynchronousQueue适用于任务直接交付给线程执行的场景,可以有效地避免任务的排队和缓冲。
          4. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。元素按照比较器或元素的自然顺序进行排序。PriorityBlockingQueue适用于需要按照优先级顺序处理任务的场景。

          注:当使用了无界队列后,maximumPoolSize会失效。

          这些BlockingQueue的区别主要在于容量限制、阻塞特性和元素排序。根据具体的需求和场景,选择合适的BlockingQueue可以提高线程池的性能和效率。

          5.5.5、threadFactory

          线程工厂。用于创建线程的工厂类。可以通过设置线程工厂来自定义线程的创建方式,例如设置线程名称、线程优先级等

          5.5.6、RejectedExecutionHandler

          拒绝策略。用于处理无法接收的任务。当线程池已满且任务无法提交时,会触发拒绝策略来处理这些任务。

          JUC提供的RejectedExecutionHandler有以下几种:

            1. AbortPolicy(默认策略):该策略会直接抛出RejectedExecutionException异常,阻止任务的提交。
            2. CallerRunsPolicy:当线程池无法接收任务时,会将任务返回给调用者执行。也就是说,由提交任务的线程来执行该任务。这样可以降低任务提交速度,但可能会影响调用线程的性能。
            3. DiscardPolicy:该策略会默默丢弃无法接收的任务,没有任何提示和异常。这可能导致任务的丢失,潜在的风险需要注意。
            4. DiscardOldestPolicy:当线程池无法接收任务时,会丢弃队列中最旧的任务,然后尝试再次提交任务。这样可以保留较新的任务,但可能会丢失一些较旧的任务。

            这些拒绝策略在处理无法接收的任务时具有不同的行为,可以根据具体的需求和业务场景选择合适的策略。需要根据任务的重要性、丢失任务的风险以及业务需求来综合考虑选择合适的拒绝策略。

            5.6、手动创建一个线程池

            private final static ThreadPoolExecutor threadPoolExecutor;
            static {
                // 这里利用hutool提供的ThreadFactoryBuilder,创建一个线程池工厂,并配置线程名称前缀
                // ThreadFactory是个接口,也可以自定义实现
                ThreadFactory threadFactory = ThreadFactoryBuilder.create().setNamePrefix("common-thread-pool-").build();
                threadPoolExecutor = new ThreadPoolExecutor(
                        // 核心线程数为7,
                        // 通常IO密集型的可以配置为2 * cpu数量
                        // CPU密集型的可以配置为 cpu数量 + 1
                        7,
                        // 最大线程数量20个
                        20,
                        // 空闲等待时间,1分钟
                        // 超过1分钟,多余的空闲线程会被销毁
                        1 * 60,
                        // 空闲等待时间,单位
                        TimeUnit.SECONDS,
                        // 有界等待队列,固定长度为50
                        // 如果使用无界队列,需要考虑内存占用问题
                        new ArrayBlockingQueue(50),
                        // 线程工厂
                        threadFactory,
                        // 拒绝策略
                        new ThreadPoolExecutor.AbortPolicy());
            }

            image.gif

            6、小结

            到这里,基本交代了线程池的一些基础概念,以及关于线程池的一些基础使用。后面的章节会讲到线程池的几个实现类,以及简单的场景使用案例。

            持续更新中......

            相关文章
            |
            7月前
            |
            存储 Java 数据安全/隐私保护
            【JUC】ThreadLocal 如何实现数据的线程隔离?
            【1月更文挑战第15天】【JUC】ThreadLocal 如何实现数据的线程隔离?ThreadLocal 导致内存泄漏问题?
            |
            7月前
            |
            安全 算法 Java
            剑指JUC原理-19.线程安全集合(上)
            剑指JUC原理-19.线程安全集合
            56 0
            |
            3月前
            |
            存储 缓存 安全
            【Java面试题汇总】多线程、JUC、锁篇(2023版)
            线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
            【Java面试题汇总】多线程、JUC、锁篇(2023版)
            |
            2月前
            |
            Java C++
            【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
            【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
            37 0
            |
            3月前
            |
            监控 Java 调度
            【Java学习】多线程&JUC万字超详解
            本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
            195 6
            【Java学习】多线程&JUC万字超详解
            |
            4月前
            |
            算法 Java
            JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
            该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
            JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
            |
            4月前
            |
            设计模式 Java 调度
            JUC线程池: ScheduledThreadPoolExecutor详解
            `ScheduledThreadPoolExecutor`是Java标准库提供的一个强大的定时任务调度工具,它让并发编程中的任务调度变得简单而可靠。这个类的设计兼顾了灵活性与功能性,使其成为实现复杂定时任务逻辑的理想选择。不过,使用时仍需留意任务的执行时间以及系统的实际响应能力,以避免潜在的调度问题影响应用程序的行为。
            91 1
            |
            4月前
            |
            Java API 调度
            JUC线程池: FutureTask详解
            总而言之,FutureTask是Java并发编程中一个非常实用的类,它在异步任务执行及结果处理方面提供了优雅的解决方案。在实现细节方面可以搭配线程池的使用,以及与Callable接口的配合使用,来完成高效的并发任务执行和结果处理。
            45 0
            |
            4月前
            |
            Java 程序员 容器
            【多线程面试题二十四】、 说说你对JUC的了解
            这篇文章介绍了Java并发包java.util.concurrent(简称JUC),它是JSR 166规范的实现,提供了并发编程所需的基础组件,包括原子更新类、锁与条件变量、线程池、阻塞队列、并发容器和同步器等多种工具。
            |
            6月前
            |
            存储 安全 Java
            Java多线程编程--JUC
            Java多线程编程