一文搞懂 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 适用于处理短生命周期的异步任务,能够动态调整线程数量以应对任务量的变化,但需要注意控制任务的数量,避免过度占用系统资源。

相关推荐
NAGNIP5 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab6 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab6 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
点光9 小时前
使用Sentinel作为Spring Boot应用限流组件
后端
不要秃头啊10 小时前
别再谈提效了:AI 时代的开发范式本质变了
前端·后端·程序员
AngelPP10 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年10 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
有志10 小时前
Java 项目添加慢 SQL 查询工具实践
后端
九狼10 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS11 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能