【Java 多线程】5 - 5 线程池

§5-5 线程池

5-5.1 Executors 线程池的工具类

导语:在以前,我们创建线程,需要创建 Thread 创建线程对象,再调用线程对象的 start 方法启动线程。一旦线程启动后,就不能够复用,也就是说,一个线程只能执行一个任务,且任务只能执行一次。而线程的创建与销毁是一项昂贵的操作,需要分配和消耗系统资源,如内存和 CPU 时间。线程池预先创建一定数量的线程,并且这些线程可以复用,只需要提交任务,线程池中的线程就会执行所提交的任务。减少了线程创建和销毁的开销,提高了并发性能。

在正式介绍使用线程池解决传统多线程编程方式的问题前,先来看看并发包(java.util.concurrent)中线程池的工具类。该工具类提供了一些创建不同类型线程池对象的工厂方法。

线程池的使用流程

  1. 创建线程池;
  2. 提交任务;
  3. 关闭线程池,释放资源;

Executors 简介

位于 java.util.concurrent,该包提供了创建 Executor, ExecutorService, ScheduledExcetutorService, ThreadFactroyCallable 类对象的工厂和工具方法。该类支持下列方法:

  • 创建并返回使用常用配置设置的 ExecutorService 对象;
  • 创建并返回使用常用配置设置的 ScheduledExecutorService 对象;
  • 创建并返回 ”包装“ 的 ExecutorService,但特定于实现的方法不可访问,重新配置已禁用;
  • 创建并返回将新建的线程设为已知状态的 ThreadFactory 对象;
  • 从其他类似闭包的形式中创建并返回 Callable 的方法,因此它们可以用于需要 Callable 的执行方法;

注意Executors 类中的所有方法都为静态方法,所有返回 ThreadFactory, ExecutorServiceScheduledExecutorService 的方法实际上返回的是一个线程池对象。

方法:这里列出类中的部分非创建线程池方法。

静态方法 描述
Callable<Object> callable(Runnable task) 返回一个 Callable 对象,调用后,执行任务并返回 null
Callable<T> callable(Runnable task, T result) 返回一个 Callable 对象,调用后,执行任务并返回指定结果

5-5.2 Executor 执行器

简介

Executor 接口位于 java.util.concurrent,表示一个执行已提交 Runnable 任务。该接口提供了一种将任务提交从任务如何执行的机制解耦的方式,包括线程使用细节、调度等。

方法

方法 描述
void execute(Runnable command) 在未来的某些时候执行指定命令

5-5.3 ThreadFactory 线程工厂

简介

ThreadFactory 接口位于 java.util.concurrent,其对象按需创建新的线程。使用线程工厂免去了手写 new Thread 的步骤,允许应用程序使用特殊的线程子类、优先级等。

该接口的最简单实现是:

class SimpleThreaedFactory implements ThreadFactory {
    public Thread newThread(Runnable r) {
        return new Thread(r);
    }
}

Executor.defaultThreadFactory() 方法提供了更实用的简单实现,它在返回之前将线程上下文设置为已知值。

接口方法

方法 描述
Thread newThread(Runnable r) 创建一条新线程

创建方法:通过 Executors 工厂类提供的工厂方法创建。

静态方法 描述
ThreadFactory defaultThreadFactory() 返回一个用于创建新线程的默认线程工厂

注意

  1. 方法会返回一个默认的线程工厂,这也是一个线程池,线程池返回的线程是空闲线程,并不会做任何事情;
  2. 接口返回的线程所执行的任务取决于接口的具体实现;
  3. 一般而言,都会将默认的线程工厂结合其他线程池使用,例如 Executors 中的 ExecutorService 工厂方法;

5-5.4 ExecutorService 执行器服务

简介

ExecutorService 接口位于 java.util.concurrent,表示一个执行器,提供管理终结(termination)、产生用于追踪一个或多个异步任务结果的 Future 对象的方法。

一个执行器服务可以被关闭(shutdown),这会导致该服务拒绝新任务。提供了两个不同的关闭执行器服务的方法。shutdown() 方法允许已提交的任务在终结前继续执行,而 shutdownNow() 会阻止等待中的线程启动,并会停止当前正在执行的任务。终结时,执行器无任何处于活跃状态的执行中任务,也没有任何等待中任务,也不能提交新任务。ExecutorService 应当在不使用时关闭,释放资源。

方法 submit 扩展了基方法 Executor.execute(Runnable),方法会创建并返回 Future,可用于取消执行,等待任务完成。方法 invokeAnyinvokeAll 执行最常用的批量执行方法,执行一系列任务,等待至少一个任务或全部任务完成。(见 ExecutorCompletionService

Executors 类提供了执行器服务的工厂方法。

接口方法

方法 描述
boolean awaitTermination(long timeout, TimeUnit unit) 阻塞至关闭请求发起后所有任务执行完成,或超时,或当前线程被打断
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) 执行给定任务,当所有任务完成时,返回一个保存状态和结果的 Future 列表
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) 执行给定任务,当所有任务完成时,或超时后,返回一个保存状态和结果的 Future 列表
T invokeAny(Collection<? extends Callable<T>> tasks) 执行给定任务,返回成功完成的结果(即不抛出异常),若有的话
T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) 执行给定任务,返回成功完成的结果(即不抛出异常),若有任务能够在超时前完成的话
boolean isShutdown() 测试这个执行器是否被关闭
boolean isTerminated() 测试关闭后所有任务是否执行完成
void shutdown() 按照任务的提交顺序启动有序关闭,但不接受新任务
List<Runnable> shutdownNow() 尝试停止所有活跃的执行中任务,中止处理等待中任务,返回等待执行的任务的列表
Future<?> submit(Runnable task) 提交一个任务,并返回一个代表该任务的 Future
Future<T> submit(Runnable task, T result) 提交一个任务,并返回一个代表该任务的 Future
Future<T> submit(Callable<T> task) 提交一个具有返回值的任务,返回一个代表任务待定结果的 Future

创建方法:通过 Executors 工具类提供的工厂方法创建。

静态方法 描述
ExecutorService newCachedThreadPool() 创建一个线程池,线程会在需要时创建,但会在可用时重用先前线程
ExecutorService newCachedThreadPool(ThreadFactory threadFactory) 创建一个线程池,线程会在需要时创建,但会在可用时重用先前线程,是用所给的线程工厂在需要时创建线程
ExecutorService newFixedThreadPool(int nThreads) 创建一个重用指定数量线程的线程池
ExecutorService newFixedTreadPool(int nThreads, ThreadFactory threadFactory) 创建一个重用指定数量线程的线程池,使用所给的线程工厂在需要时创建线程
ExecutorService newSingleThreadExecutor() 创建一个使用一条工作线程的执行器
ExecutorService newSingleThreadExecutor(ThreadFactroy threadFactory) 创建一个使用一条工作线程的执行器,是用所给的线程工厂在需要时创建线程
ExecutorService newThreadPerTaskExecutor(ThreadFactory factory) 创建一个为每个任务启动新线程的执行器
ExecutorService newWorkStealingPool() 创建一个工作窃取线程池,默认将可用处理器数量作为目标并行级别(数)
ExecutorService newWorkStealingPool(int parallelism) 创建一个支持指定并行级别的工作窃取线程池,可能使用多条队列减少竞争

注意

  1. newCachedThreadPool() 方法:方法所返回的线程池对象不限线程数量(实际为 Integer.MAX_VALUE);

  2. newFixedThreadPool(int) 方法:方法会限定线程池中最大线程数,若提交的任务数量多于最大线程数,则待处理的任务会在存储在一个共享的无界队列中等待;

    同样地,这个队列也存在于 newSingleThreadExecutor 方法所返回的线程池中;

    这两个方法返回的实现类为 ThreadPoolExecutor

  3. newThreadPerTaskExecutor(ThreadFactory) 方法:该方法自 Java 21 起被引入,该执行器所创建的线程数是无界的;

    该方法返回的实现类为 java.util.concurrent.ThreadPerTaskExecutor

  4. newWorkStealingPool() 方法:方法会返回一个工作窃取线程池,该线程池的线程调度策略是,当一个线程完成了其所有任务时,它会尝试从其他线程队列中 ”窃取“ 未完成的工作来执行。这样做可以使得线程一直保持忙碌,避免线程空闲和等待,有效均衡负载,提高并发性能;

    调用该方法,实际上返回的是接口的实现类 ForkJoinPool,这是一个实现了工作窃取算法的线程池,每一条线程都会分配一个双端队列,用于存放需要执行的任务。窃取时,实际上就是从这个工作队列中窃取一个任务来执行;

    由于会发生工作窃取,因此可能会发生并发安全问题。一般而言,线程自己的工作队列(本地队列)采取后进先出的出入顺序,窃取时采取先入先出的出入顺序;

  5. shutdown() 方法:若不关闭,即使线程工作完成,程序不会停止;若想要程序停止,则应当关闭线程池;

示例:一个简单的 ExecutorService 示例。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo implements Runnable {
    public static void main(String[] args) {
        //测试线程池
        //使用 Executors 工具类返回线程池
        //固定大小线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);	//ThreadPoolExecutor
        //提交任务,可在每次提交后在主线程中插入 Thread.sleep(long) 更方便地查看线程池中线程情况
        fixedThreadPool.submit(new ThreadPool());
        fixedThreadPool.submit(new ThreadPool());
        fixedThreadPool.submit(new ThreadPool());
        fixedThreadPool.submit(new ThreadPool());
        fixedThreadPool.submit(new ThreadPool());
        //释放资源
        fixedThreadPool.shutdown();
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}

5-5.5 ScheduledExecutorService 计划执行器服务

简介

ScheduledExecutorService 接口位于 java.util.concurrent,是 ExecutorService 的子接口。

ScheduledExecutorService 是一个能计划命令在指定延迟后执行,或周期性执行的 ExecutorService

schedule 方法创建具有不同延迟的任务,并返回一个可用于取消和检查执行情况的任务对象。scheduleAtFixedRatescheduleWithFixedDelay 方法创建并执行周期性任务,直至取消为止。

使用 Executor.execute(Runnable)ExecutorServicesubmit 方法提交的任务的延迟为 0。允许在 schedule 方法指定非正延迟(不是周期),这会视为立即执行。

所有 schedule 方法参数接受相对延迟和周期,而不是绝对时间和日期。将由 Date 表示的绝对时间转换为所要求的形式并不困难。例如,若计划在未来某个时间 date,可以使用:schedule(task, date.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS)。但注意,由于网络时间同步协议、时钟漂移或其他因素,相对延迟超时不要与启用任务的当前日期一致。

Executors 类提供了创建 ScheduledExecutorService 的便捷工厂方法。

接口方法:接口继承了 ExecutorService,可以调用其父接口的方法(如 submit, invokeAny 等)。

方法 描述
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) 提交一次性任务,在指定延迟后启用任务
ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) 提交具有返回值的一次性任务,在指定延迟后启用任务
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 提交一个周期性任务,任务在指定初始延迟后启用,然后按指定周期循环执行
ScheduledFuture<?> scheduleAtFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 提交一个周期性任务,任务在指定初始延迟后启用,然后在一个执行结束和下一个执行开始之间的给定延迟期间启用

注意

  1. scheduledAtFixedRate() 方法:第一次任务执行会在 initialDelay 后开始,第二次任务执行会在 initialDelay + period 后开始,第三次任务执行会在 initialDelay + 2 * period 后开始,以此类推;
  2. 所有 schedule 方法都会返回一个代表任务执行情况的 Future 对象;

创建方法:通过 Executors 工具类创建。

静态方法 描述
ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个可以计划任务延迟执行或周期性执行的线程池
ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) 创建一个可以计划任务延迟执行或周期性执行的线程池
ScheduledExecutorService newSingleThreadScheduledExecutor() 创建一个单线程池,可以计划任务延迟执行或周期性执行
ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) 创建一个单线程池,可以计划任务延迟执行或周期性执行

注意:方法返回的实现类为 ScheduledThreadPoolExecutor,是 ThreadPoolExecutor 的子类。

示例:一个简单的 ScheduledExecutorService 示例。

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPool implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello world!");
    }

    public static void main(String[] args) throws InterruptedException {
        //创建线程池
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3, Executors.defaultThreadFactory());
        //计划线程执行
        //一次性任务
        scheduledThreadPool.schedule(() -> System.out.println("Hello Java"), 1, TimeUnit.SECONDS);
        //周期性任务
        scheduledThreadPool.scheduleAtFixedRate(new ScheduledThreadPool(), 2, 1, TimeUnit.SECONDS);

        //关闭
        scheduledThreadPool.awaitTermination(10, TimeUnit.SECONDS);
        scheduledThreadPool.shutdown();
    }
}

5-5.6 使用 ThreadPoolExecutor 自定义线程池

ThreadPoolExecutor 位于 java.util.concurrent,是 ExecutorService 的一个实现类,也是 Executors 工具类中返回值类型为 ExecutorService 的实现类对象。

一般使用 Executors 所提供的工具类方法创建一个预设好的 ThreadPoolExecutor 线程池实例。但也可以直接调用其构造器,自定义线程池。一旦创建成功,其使用方法与 Executors 返回的线程池无异。

ThreadPoolExecutor 简介

ThreadPoolExecutor 是一个使用线程池中可能多条线程中的一条,执行所提交任务的 ExecutorService,通常由 Exectors 的工具类配置。

线程池处理两个不同的问题:由于减少了每个任务的调用开销,线程池通常在处理大量异步任务时具有良好的性能,线程池还提供了绑定和管理执行任务集合时消耗的资源(包括线程)的方法。每一个 ThreadPoolExecutor 也维护一些基本统计信息,例如已完成任务数。

为了使线程池能够在广泛的上下文发挥作用,该类提供了许多可调参数和延长性钩子(extensibility hooks)。然而,我们呼吁程序员使用更为方便的 Executors 工厂方法 Executors.newCachedThreadPool()(无界线程池,自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池),和 Executors.newSingleThreadExecutor()(单背景线程)。这些都为多种常见使用情形做好了预设。

构造方法ThreadPoolExecutor 的构造器具有多种重载,但是无论是哪种重载,最终调用的都是其最多参数重载的构造器。因此,这里只介绍这一种重载。

构造方法 描述
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 使用所给的初始参数,创建一个新的 ThreadPoolExecutor 实例

参数介绍

  1. corePoolSize:核心线程数,线程池会优先将任务交给核心线程执行;
  2. maximumPoolSize:最大线程数,线程池中允许的最大线程数量,临时线程数为最大线程数与核心线程数之差;
  3. keepAliveTime:临时线程的存活时间(值),决定了临时线程在空闲多长时间后被回收;
  4. unit:临时线程存活时间的单位,由枚举类中的枚举常量决定;
  5. workQueue:线程工作队列,是一个阻塞队列,用于任务的排队等待;
  6. threadFactory:线程工厂,指示了新建线程的方式,一般调用 Executors.defautThreadFactory() 返回;
  7. handler:任务拒绝策略,该策略决定了当线程池所有线程忙碌且队列已满时,超额任务的处理策略;

任务拒绝策略

任务拒绝策略由接口 RejectedExecutionHandler 决定,其所有实现类都为 ThreadPoolExecutor 的静态内部类。

  • AbortPolicy:放弃策略,抛弃任务并抛出 RejectedExecutionException,这也是默认策略
  • CallerRunsPolicy:调用者执行策略,由调用线程直接执行任务(绕过线程池),若执行器被关闭,则任务被抛弃;
  • DiscardOldestPolicy:抛弃最长等待策略,抛弃队列中等待时间最长的任务,并让该任务插入队列中,若执行器被关闭,则任务被抛弃;
  • DiscardPolicy:抛弃策略,静默抛弃任务,不推荐这种做法

线程池工作原理

首先,会创建一个空线程池。一旦提交了任务,线程池就会创建线程执行任务,且优先用核心线程执行。

当核心线程已满时,再次提交任务,任务就会在阻塞队列中排队等待。

若核心线程和队列都已满时,则会创建临时线程执行任务。因此,任务的执行并不一定根据任务提交顺序进行。

当线程池满负荷运行时(核心线程、工作队列、临时线程都已满),再次提交任务则会触发任务拒绝策略。

选择合适的线程池大小

在谈论合适的线程池大小前,现邀请出一个概念:最大并行数

最大并行数在前文的工作窃取线程池中已有提及。在多线程和并发编程中,最大并行数(maximum parallelism)指的是在给定的硬件环境下,能够同时执行的线程或任务的最大数量。这一概念沿用到了许多其他需要并发控制的,特别是需要高并发的场景下,例如数据库、银行、电商等。

在 Windows 上,打开任务管理器,转到 “性能” 选项卡,CPU 性能信息中的 “逻辑处理器” 数目就是 CPU 的最大并行数。还可以通过设备管理器的 “处理器” 一栏查看处理器数目以确定 CPU 的最大并行数。

然而,一些操作系统并不会让某一个应用程序能够完全调用 CPU,也就是说,一个应用程序的最大并行数可能不等于 CPU 的最大并行数。Java 提供了一种查看虚拟机最大并行数的方法:Runtime.getRuntime().availableProcessors()。这一方法可查看 JVM 能够使用多少可用处理器数目。默认情况下,工作窃取线程池的最大并行数就是通过该方法确定。

有了这一概念,接下来讨论在两种不同情况下的线程池大小计算。

CPU 密集型:CPU 密集型指的是需要 CPU 进行大量计算,但是读取文件和读取数据库的操作较少的任务。这种运算任务的线程池大小为:

\[\text{最大并行数} + 1 \]

之所以要这样设置,是为了在出现页缺失故障或其他某些原因导致线程暂停时,还能有线程 “替补” 上去。

I/O 密集型:I/O 密集型指的是需要频繁、大量访问 I/O 设备的运算任务,这样的运算任务往往会因为需要等待 I/O 设备而导致 CPU 有大量时间被浪费在了等待 I/O 就绪的上。这种类型的任务也是现如今大多数的项目的任务特点。这种运算任务的线程池大小为:

\[\text{最大并行数} \times \text{期望CPU利用率} \times \frac{\text{CPU计算时间} + \text{等待时间}}{\text{CPU 计算时间}} \]

这种设置能够使得 CPU 在等待 I/O 设备时,使用多线程技术提高 CPU 利用率,提高并发性能。

而 CPU 的计算时间和等待时间是未知的,我们可以使用工具 Thread Dump 测试得出,这属于性能分析内容,这里不做展开。

5-5.7 创建虚拟线程池

虚拟线程是 Java 21 新引入的功能,在介绍虚拟线程和虚拟线程池的使用方法前,有必要先来了解什么是虚拟线程。

详细信息请见 Java 核心库文档并发篇中的虚拟线程一文。

Virtual Threads (oracle.com)

平台线程(platform thread):平台线程作为操作系统线程(OS thread)的薄包装线程实现的。平台线程(用户态)在其依赖的底层操作系统线程(内核态)执行 Java 代码。平台线程会在其整个生命周期内捕获其操作系统线程。通常而言,操作系统线程数限制了可用的平台线程数。一个应用程序不能够不受限制地无限创建平台线程。

平台线程通常具有一个大型线程栈和由操作系统所维护的其他资源,适用于执行所有类型的任务,但可能是有限的资源。

虚拟线程(virtual thread):虚拟线程是一种轻量级的线程,减少了编写、维护、调式高吞吐量并发应用程序的工作量。同平台线程,虚拟线程属于 java.lang.Thread 的一个实例。虚拟线程的实现方式类似于虚拟内存,要模拟大量线程(虚拟线程),Java 运行时会将大量的虚拟线程映射到数量较少的操作系统线程上。使用虚拟线程,可大幅提高应用程序的吞吐量,但这并不意味着速度的提升。虚拟线程适用于绝大多数时间都在 I/O 阻塞的 I/O 密集型任务中,而不是 CPU 密集型任务。

一条虚拟线程并不会和某一条具体的操作系统线程绑定,但虚拟线程的代码仍然在操作系统线程上执行。当正在执行代码的虚拟线程调用了 I/O 阻塞操作,Java 运行时会将该虚拟线程挂起,直至其能够继续执行。关联了挂起的虚拟线程的操作系统线程就能够执行其他虚拟线程的操作。

由于虚拟线程是由 JDK 调度的,减少了频繁上下文切换和创建线程的开销,在一定程度上提高了性能。再次强调,虚拟线程适用于高吞吐量的并发应用程序中,尤其是花费大量时间在等待上的大量并发任务中。虚拟线程只是提高了吞吐量,并不是执行速度更快的线程。

虚拟线程并没有线程名称(为空字符串),其线程优先级固定不可变,同时也是守护线程,不会阻止关机序列。

Java 虚拟线程与 Go 的协程的区别:二者都是轻量级的线程,都可以帮助开发者实现并发编程。但是它们的工作原理和用途略有不同。Java 虚拟线程是由 Java 虚拟机内部实现,可帮助提高 Java 应用程序的执行效率,但是它们不能跨越多个处理器或核心执行。而 Go 的协程是在操作系统内部实现的,可以跨越多个处理器或核心执行,且 Go 的协程具有更为灵活的内存管理机制,但它的运行效率可能不如虚拟线程。(引用自:Java的虚拟线程和go的协程哪个好_胡说先森的博客-CSDN博客

创建虚拟线程:单条虚拟线程通过 Thread 中的 Thread.Builder API 创建,同样地,平台线程也可以由线程构造器构造。

静态方法 描述
Thread startVirtualThread(Runnable task) 创建一条虚拟线程并调度执行该线程
Thread.Builder.OfVitual ofVirtual() 返回一个可用于创建虚拟线程或虚拟线程工厂的构造器
Thread.Builder.OfPlatform ofPlatform() 返回一个可用于创建平台线程或平台线程工厂的构造器

线程构造器常用方法:这两个常用方法都来自于构造器接口。

方法 描述
ThreadFactory factory() 从当前构造器中返回一个线程工厂,用于创建线程
Thread start(Runnable task) 从当前构造器中创建一条新线程,并调度其执行

提示:通常上述两个方法会采用链式编程(连点式)使用。

创建虚拟线程池:除了 Thread.Builder.OfVirtual().factory() 外,还可通过 Executors 工具类创建。

静态方法 描述
ExecutorService newVirtualThreadPerTaskExecutor() 创建一个为每个任务启动一条新的虚拟线程的执行器

注意

  • 该方法返回的执行器,是一个无界的线程池;
  • 该方法实际等价于 return newThreadPerTaskExecutor(Thread.ofVirtual().factory)
  • 方法返回的执行器服务的实际运行时类同 Executors.newThreadPerTaskExecutor(ThreadFactory),为 ThreadPerTaskExecutor

示例:一个简单的虚拟线程池示例。

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ExecutorService;

public class VirtualThreads {
    public static void main(String[] args) {
        // 虚拟线程测试
        ExecutorService virtualPool = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 0; i < 1000; i++) {
            virtualPool.submit(() -> {
                System.out.println("Hello world");
                try {
                    Thread.sleep(1000);
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
            });
        }
    }
}

5-5.8 链接

Java 线程池,工作窃取算法:java线程池,工作窃取算法 - KAnts - 博客园 (cnblogs.com)

Virtual Threads:Virtual Threads (oracle.com)

Java 的虚拟线程和 Go 的协程哪个好:Java的虚拟线程和go的协程哪个好_胡说先森的博客-CSDN博客

posted @ 2023-09-05 17:17  Zebt  阅读(122)  评论(0)    收藏  举报