一文搞懂 java 线程池:SingleThreadExecutor 和 CachedThreadPool 原理

你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:

  1. 了解大厂经验
  2. 拥有和大厂相匹配的技术等

希望看什么,评论或者私信告诉我!

一、前言

上一章节我们详解介绍了ThreadPoolExecutor 和 FixedThreadPool 的原理以及应用场景,本章我们继续介绍 SingleThreadExecutorCachedThreadPool

二、线程池

2.1 SingleThreadExecutor

2.1.1 SingleThreadExecutor 简介

SingleThreadExecutor是使用单个线程的线程池,源码实现

java 复制代码
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。

其他参数与FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。SingleThreadExecutor使用无界队列作为工作队列对线程池带来的影响与FixedThreadPool相同。

2.1.2 SingleThreadExecutor 的 execute 方法原理

  1. 如果当前运行的线程数少于corePoolSize(即线程池中无运行的线程),则创建一个新线程来执行任务。
  2. 在线程池完成预热之后(当前线程池中有一个运行的线程),将任务加入LinkedBlockingQueue。
  3. 线程执行完1中的任务后,会在一个无限循环中反复从LinkedBlockingQueue获取任务来执行。

2.1.3 为什么需要 SingleThreadExecutor

SingleThreadExecutor 是一个只有单一工作线程的 Executor,它可以有效地处理需要顺序执行的一系列任务,以及需要在线程之间保持顺序的场景。虽然看上去似乎可以直接使用单一线程来完成相同工作,但 SingleThreadExecutor 提供了一些额外的优势和适用性:

  1. 线程生命周期管理:SingleThreadExecutor 管理着单一工作线程的生命周期,当有任务需要执行时,会创建线程;任务执行完毕后线程不会立即销毁,而是保留在线程池中等待下一个任务。这样可以避免频繁地创建和销毁线程带来的开销。
  2. 顺序执行:SingleThreadExecutor 保证任务按照提交的顺序依次执行,不会产生并发访问的问题。这对一些要求顺序执行的任务非常重要。
  3. 线程鲁棒性:由于 SingleThreadExecutor 只有一个线程,可以减少并发问题的复杂性,特别是在数据共享和状态管理方面,避免了多线程同时访问共享资源带来的同步和竞争问题。
  4. 易于调试:在 SingleThreadExecutor 中,由于只有一个线程在执行任务,当任务出现问题时容易进行调试和定位。
  5. 任务队列管理:SingleThreadExecutor 中的任务可以被放入队列中等待执行,可以有效管理任务的执行顺序和流量控制。
  6. 适用性:对于某些场景,需要保证任务的顺序执行和结果一致性的情况下,

SingleThreadExecutor 提供了一个简单且高效的解决方案。 尽管在简单情况下直接使用单一线程可能够用,但 SingleThreadExecutor 提供了更多的控制和管理能力,特别是在复杂或者需要顺序执行的情况下,它能够更好地满足这些需求并提供更好的可维护性和性能。因此,SingleThreadExecutor 在一些特定场景下是非常有价值的。

2.1.4 单线程能够替代 SingleThreadExecutor 吗

在某些情况下,使用单线程可能可以替代 SingleThreadExecutor,但在其他情况下,SingleThreadExecutor 提供的管理功能和灵活性仍然是有价值的。下面是一些关键点来比较单线程和 SingleThreadExecutor 的替代性:

单线程的优势和适用性:

  1. 简单性:单线程相对简单,没有线程管理的复杂性,适用于一些简单的任务执行场景。
  2. 资源开销:如果任务较简单且不需要额外的线程管理,则单线程可以节省资源开销。
  3. 顺序执行:单线程天然是顺序执行的,适合于一些需要顺序执行的任务。

何时可以考虑单线程替代 SingleThreadExecutor:

  • 简单任务:处理简单的任务且不需要线程池的额外功能时,单线程可以代替 SingleThreadExecutor。
  • 顺序执行:如果任务需要按照提交顺序依次执行,单线程可以胜任这种情况。

SingleThreadExecutor 的优势和适用性:

  1. 任务队列:SingleThreadExecutor 可以使用任务队列管理任务,确保任务按照顺序执行。
  2. 任务管理:单线程池可以更好地管理任务的执行,避免因任务耗时或异常导致整个系统被阻塞。
  3. 线程复用:SingleThreadExecutor 可以复用线程,减少线程创建销毁的开销。
  4. 异常处理:线程池可以提供更好的异常处理机制,避免因为异常导致整个进程崩溃。

何时适合使用 SingleThreadExecutor:

  • 复杂任务:对于复杂的任务管理和执行场景,使用 SingleThreadExecutor 可以更好地控制任务的执行和管理。
  • 任务队列管理:需要使用任务队列管理并控制任务执行顺序时,SingleThreadExecutor 更适合。

总的来说,单线程可以在一些简单的、不需要额外线程管理功能的情况下替代 SingleThreadExecutor。然而,当需要更多的灵活性、管理功能和高级特性时,SingleThreadExecutor 仍然是更好的选择,特别是在需要任务队列管理、异常处理、资源控制等方面的场景中。

2.1.5 SingleThreadExecutor 使用场景

SingleThreadExecutor 是一个只有一个工作线程的线程池,它保证所有提交的任务都是顺序执行的。这种线程池的使用场景通常适合以下情况:

  1. 顺序执行任务: 当需要确保所有任务按照提交的顺序依次执行时,可以使用 SingleThreadExecutor。这样可以避免多线程情况下可能出现的并发问题,保证任务的顺序性。
  2. 适用于后台任务: 如果有一些需要按顺序执行的后台任务,例如日志记录、定时执行的任务等,可以通过 SingleThreadExecutor 来实现。
  3. 资源共享的情况: 在一些资源共享的场景下,如读取共享的数据或操作共享的资源,使用单线程可以简化同步和控制,并且避免资源争用。
  4. 保证任务之间不受影响: 某些任务可能会造成阻塞,如果其他任务不能受到这种影响,可以使用单线程来确保这种情况。
  5. 任务处理较快: 当任务处理速度较快,且需要按照特定顺序执行时,使用 SingleThreadExecutor 不会引入多线程开销,保持了简单性和可靠性。

总的来说,SingleThreadExecutor 在需要控制串行执行和避免并发问题的场景下非常有用。它提供了针对这种需求的简单且可靠的解决方案。

2.2 CachedThreadPool

2.2.1 CachedThreadPool 简介

CachedThreadPool是一个会根据需要创建新线程的线程池。下面是创建CachedThreadPool的源代码。

java 复制代码
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

CachedThreadPool的corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为 Integer.MAX_VALUE,即maximumPool是无界的。这里把keepAliveTime设置为60L,意味着 CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被 终止。

FixedThreadPool和SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的 工作队列。CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但 CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于 maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下, CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源

2.2.2 CachedThreadPool的execute()方法原理

  1. 首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线程 正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方 法执行完成;否则执行下面的步骤2

  2. 当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1)将失 败。此时CachedThreadPool会创建一个新线程执行任务

  3. 在步骤 2 中新创建的线程将任务执行完后,会执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执 行步骤1),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于 空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

2.2.3 CachedThreadPool优缺点和应用场景

CachedThreadPool 是 Java 中线程池的一种实现,其优缺点和使用场景如下:

优点:

  1. 动态调整线程数量:
    • CachedThreadPool 可根据需要动态创建和回收线程,适合短生命周期的异步任务,避免长时间维持多余线程造成资源浪费。
  2. 灵活性:
    • 可以根据需求自动调整线程池的大小,避免过载或资源浪费。
  3. 高效利用资源:
    • 可以重复利用线程,避免线程创建和销毁的开销。

缺点:

  1. 线程数无限增长:
    • CachedThreadPool 可以无限制地创建新线程,当任务数量过多时可能导致大量线程同时运行,增加系统负担。
  2. 缺乏控制:
    • 由于线程数量动态调整,缺乏对线程数量的精确控制,可能导致系统资源被过度占用。
  3. 可能会造成资源消耗过多:
    • 对于一些长时间执行、密集计算的任务,可能会出现因为创建过多线程而消耗过多系统资源的情况。

使用场景:

  1. 短期生命周期的任务
    • 适用于短时间执行的任务,能够减少线程的创建和销毁开销。
  2. 系统负载较低:
    • 当系统负载较低、且任务需要较快响应时,可以使用 CachedThreadPool 以提高任务执行效率。
  3. 短暂且并发度高的情况:
    • 例如,处理短时间的请求或多个独立小任务时,CachedThreadPool 能够动态调整线程数量,提高并发度。

总的来说,CachedThreadPool 适合处理大量短生命周期、异步并发任务的场景,能够高效地管理线程资源,但需要注意控制任务的数量,避免过度占用系统资源。

2.3 扩展

SynchronousQueue 是 Java 中的一个特殊类型的队列实现,它是一个没有存储元素的阻塞队列。在 SynchronousQueue 中,每个插入操作必须等待另一个线程进行对应的删除操作,反之亦然。这意味着在 SynchronousQueue 中,生产者线程和消费者线程必须同时准备好,否则操作无法执行,这种特性使其成为一种高效的线程间通信工具。

以下是关于 SynchronousQueue 的一些详细解释:

  1. 特性

    • SynchronousQueue 是一种零容量的队列,插入元素和移除元素必须同时发生。
    • 插入操作(put)必须等待另一个线程尝试删除元素(take)。
    • 同样,删除操作(take)必须等待另一个线程尝试插入元素(put)。
  2. 使用场景

    • 主要用于线程之间的直接交互,特别是在生产者和消费者要同步的时候使用。
    • 当生产者线程和消费者线程必须同时准备好才能执行操作时,可以使用 SynchronousQueue
  3. 工作原理

    • 当生产者试图将元素放入队列时(通过 put 方法),它会等待,直到有另一个线程尝试从队列中获取元素(通过 take 方法)。
    • 当消费者试图从队列中获取元素时(通过 take 方法),它会等待,直到有另一个线程尝试将元素放入队列(通过 put 方法)。
  4. 代码示例

    java 复制代码
    SynchronousQueue<Integer> queue = new SynchronousQueue<>();
    
    new Thread(() -> {
        try {
            Integer element = 1;
            queue.put(element);
            System.out.println("Produced: " + element);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }).start();
    
    new Thread(() -> {
        try {
            Integer element = queue.take();
            System.out.println("Consumed: " + element);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }).start();

在上述代码示例中,一个线程在放入元素到 SynchronousQueue 中,而另一个线程则从中获取元素,它们必须同时准备好才能完成交互操作。

总的来说,SynchronousQueue 是一个在多线程并发操作中非常有用的同步机制,适合需要严格控制生产者和消费者之间协同的情况。

三、总结

SingleThreadExecutor 和 CachedThreadPool 是 Java 中两种重要的线程池实现。SingleThreadExecutor 适用于需要顺序执行任务和保持任务之间顺序性的场景,提供了线程生命周期管理、顺序执行、线程鲁棒性、易于调试和任务队列管理等功能。CachedThreadPool 适用于处理短生命周期的异步任务,能够动态调整线程数量以应对任务量的变化,但需要注意控制任务的数量,避免过度占用系统资源。

相关推荐
FIN技术铺4 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序11 分钟前
vue3 封装request请求
java·前端·typescript·vue
凡人的AI工具箱26 分钟前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
陈王卜29 分钟前
django+boostrap实现发布博客权限控制
java·前端·django
小码的头发丝、29 分钟前
Spring Boot 注解
java·spring boot
java亮小白199734 分钟前
Spring循环依赖如何解决的?
java·后端·spring
飞滕人生TYF41 分钟前
java Queue 详解
java·队列
果冻人工智能42 分钟前
2025 年将颠覆商业的 8 大 AI 应用场景
人工智能·ai员工
代码不行的搬运工44 分钟前
神经网络12-Time-Series Transformer (TST)模型
人工智能·神经网络·transformer
石小石Orz1 小时前
Three.js + AI:AI 算法生成 3D 萤火虫飞舞效果~
javascript·人工智能·算法