JDK21 虚拟线程详解,

大纲

1. 虚拟线程是什么
2. 虚拟线程缺点与潜在问题
3. 虚拟线程创建的几种方法
4. 虚拟线程使用场景

一、虚拟线程是什么

Java 虚拟线程(Virtual Threads)是一种在 Java 虚拟机(JVM)内部实现的轻量级线程机制,旨在为 Java 应用程序提供高效、易用且高度可扩展的并发模型。

核心特点

1.轻量化:
  • 虚拟线程具有极低的创建和上下文切换成本,相比于传统的操作系统(OS)线程(也称作“平台线程”),它们占用更少的内存资源,使得应用程序能够轻松地创建和管理数千甚至数百万个并发线程。
  • 这种轻量化设计特别适用于处理大量并发请求、网络IO密集型应用或事件驱动编程场景,其中每个请求或事件可能对应一个独立的执行单元。
2.M:N 调度模型:
  • 虚拟线程遵循 M:N 调度策略,即有 M 个虚拟线程被 JVM 内部的调度器映射到 N 个平台线程(通常是 OS 线程)上运行。
  • JVM 负责虚拟线程的调度,将其有效地分发到有限数量的平台线程上。这样,即使有大量的虚拟线程,实际消耗的操作系统资源(如内核线程)仍然保持在一个可控范围。
3.连续传递样式 (CPS):
  • 实现虚拟线程的技术之一可能是连续传递样式(Continuation Passing Style, CPS)。这是一种编程范式,通过将程序控制流作为参数在函数间传递来支持非阻塞式的协同式多任务执行。
  • 使用 CPS 可以简化对异步操作的管理和编排,减少因等待IO操作完成而引起的阻塞,从而提升并发性能。
4.易于编程:
  • 开发者可以像使用标准 Thread 类一样创建和管理虚拟线程,使用熟悉的同步原语(如 synchronized 关键字、Lock 接口等)进行线程间通信和同步。
  • 虽然底层实现有所不同,但在编程接口和体验上,虚拟线程力求与传统线程保持一致,降低学习和迁移成本。

工作原理概览

  • 创建:开发者创建一个虚拟线程实例,如同创建普通 Java 线程一样,但底层创建的是轻量级的虚拟线程对象而非操作系统线程。
  • 调度:JVM 的调度器负责将这些虚拟线程分配到已有的平台线程上执行。当一个虚拟线程因等待 IO 操作或其他原因而阻塞时,JVM 能够透明地将其从当前平台线程上移除,释放平台线程去执行其他虚拟线程,从而避免了不必要的上下文切换开销。
  • 协作与抢占:虽然虚拟线程的设计倾向于避免显式的锁竞争和上下文切换,但在必要时,JVM 调度器也可以实现基于时间片的抢占式调度,确保所有虚拟线程公平地获得执行机会。
  • 线程本地存储:类似于传统线程,虚拟线程同样支持线程本地存储(ThreadLocal),以便在每个线程上下文中保存特定于线程的数据。

二、虚拟线程的缺点与潜在问题

三、创建虚拟线程的几种方法

1.Thread的静态方法

以下两种都是等效的

Thread.ofVirtual().start(Runnable task);

Thread.startVirtualThread(Runnable task)

都是使用ThreadBuilders的newVirtualThread方法来创建虚拟线程

由于Thread.startVirtualThread是在方法体里面直接帮我们填好了虚拟线程所需要的参数,所以无法给我们自己的虚拟线程命名

    public static Thread startVirtualThread(Runnable task) {
        Objects.requireNonNull(task);
        var thread = ThreadBuilders.newVirtualThread(null, null, 0, task);
        thread.start();
        return thread;
    }

但是我们可以使用Thread.ofVirtual()来创建自定义名字等参数的虚拟线程

直接看有哪些方法

static final class VirtualThreadBuilder
extends BaseThreadBuilder implements OfVirtual {
    private Executor scheduler;

    VirtualThreadBuilder() {
    }

    // invoked by tests
    VirtualThreadBuilder(Executor scheduler) {
        if (!ContinuationSupport.isSupported())
            throw new UnsupportedOperationException();
        this.scheduler = Objects.requireNonNull(scheduler);
    }

    @Override
    public OfVirtual name(String name) {
        setName(name);
        return this;
    }

    @Override
    public OfVirtual name(String prefix, long start) {
        setName(prefix, start);
        return this;
    }

    @Override
    public OfVirtual inheritInheritableThreadLocals(boolean inherit) {
        setInheritInheritableThreadLocals(inherit);
        return this;
    }

    @Override
    public OfVirtual uncaughtExceptionHandler(UncaughtExceptionHandler ueh) {
        setUncaughtExceptionHandler(ueh);
        return this;
    }

    @Override
    public Thread unstarted(Runnable task) {
        Objects.requireNonNull(task);
        var thread = newVirtualThread(scheduler, nextThreadName(), characteristics(), task);
        UncaughtExceptionHandler uhe = uncaughtExceptionHandler();
        if (uhe != null)
            thread.uncaughtExceptionHandler(uhe);
        return thread;
    }

    @Override
    public Thread start(Runnable task) {
        Thread thread = unstarted(task);
        thread.start();
        return thread;
    }

    @Override
    public ThreadFactory factory() {
        return new VirtualThreadFactory(scheduler, name(), counter(), characteristics(),
                                        uncaughtExceptionHandler());
    }
}

看到其代码可发现有name、inheritInheritableThreadLocals、uncaughtExceptionHandler方法是可以选的,最后调用start或者unstart方法即可启动线程

2.线程工厂

ThreadBuilders.VirtualThreadBuilder()提供的创建线程工厂的方法

ThreadFactory factory = Thread.ofVirtual().factory();

在其前面也可以调用name方法,来让工厂创建的虚拟线程都是按照同一名字创建

        ThreadFactory factory = Thread
                .ofVirtual()
                .name("vt")
                .factory();

可以使用Executors.newVirtualThreadPerTaskExecutor来创建虚拟线程线程工厂

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

但其实际也是通过Thread.ofVirtual().factory()来创建线程工厂

    public static ExecutorService newVirtualThreadPerTaskExecutor() {
        ThreadFactory factory = Thread.ofVirtual().factory();
        return newThreadPerTaskExecutor(factory);
    }

但这就有个问题了,看上面代码,创建的是一个ThreadPerTaskExecutor,意味着每一次提交的任务都是创建一个新的虚拟线程去执行,而不是复用之前的线程。那虚拟线程怎么复用呢?

那就需要我们自己实现线程池去管理虚拟线程

现在JDK没有提供复用虚拟线程的方法,我猜可能也是设计虚拟线程的时候就是本着一个虚拟线程一个任务去做的。

我也写了一个demo来循环使用同一个虚拟线程,看看就行。实际使用还是只完成单独一个任务即可

public class demo {

    private static volatile LinkedList<Runnable> workQueue = new LinkedList<>();
    private static int count = 0;


    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, InterruptedException {
        Thread thread1 = Thread.ofVirtual().name("c-vt").start(() -> {
            while (true) {
                boolean interrupted = Thread.interrupted();
                if (interrupted) {
                    break;
                }
                if (!workQueue.isEmpty()) {

                    Runnable runnable = workQueue.removeFirst();
                    runnable.run();

                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        Thread thread2 = Thread.ofVirtual().name("p-vt").start(() -> {
            while (true) {
                boolean interrupted = Thread.interrupted();
                if (interrupted) {
                    break;
                }
                workQueue.addLast(() -> {
                    System.out.println("任务 " + count++ + "queue size = " + workQueue.size());
                });
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        Thread.sleep(10000);
        thread1.interrupt();
        thread2.interrupt();
        Thread.sleep(200);
        System.out.println("main final queue size = "+workQueue.size());


    }

}

四、虚拟线程的使用场景

一下是AI提供的场景

  1. 高并发场景:
  • 在处理大量短生命周期、I/O 密集型任务时,如网络请求、文件读写或数据库查询,虚拟线程特别有用。由于它们不需要消耗操作系统级别的资源(如内核线程),可以轻松创建并同时运行数千甚至数百万个虚拟线程,极大地提高了系统的并发能力,进而提升整体吞吐量。
  1. 响应式编程与异步服务:
  • 在构建响应式应用程序或异步微服务架构中,虚拟线程能够简化异步编程模型。开发者可以使用类似于传统同步编程风格编写代码,但底层通过虚拟线程实现非阻塞执行,避免了复杂的回调和 Future/CompletableFuture 管理,提升了代码可读性和维护性。
  1. 事件驱动框架:
  • 在事件处理器、消息队列消费者等场景中,每当有新事件到达时,通常会启动一个新线程进行处理以保证响应速度。采用虚拟线程,可以显著降低因频繁创建和销毁线程带来的开销,特别是在事件密集且处理逻辑较轻量的情况下。
  1. Web 服务器与API服务:
  • 对于高负载的 Web 服务器和 API 服务,每个请求通常在一个单独的线程上处理。使用虚拟线程,服务器可以轻松应对海量并发连接,而无需担心线程资源耗尽或过度优化线程池大小。这有助于提高服务的可伸缩性和性能。
  1. 大数据处理与流计算:
  • 在处理大规模数据流或批处理作业时,虚拟线程可以帮助分解任务为大量细粒度的工作单元,并行执行这些单元。由于虚拟线程的低成本特性,可以在不增加系统负担的前提下实现近乎无界的并行度,从而加速数据处理流程。
  1. 测试与模拟:
  • 在进行并发测试、压力测试或模拟高并发用户行为时,虚拟线程使得快速生成大量并发线程变得简单且成本低廉。这有助于更准确地评估系统在极端并发条件下的表现,而不必担心实际环境中难以复现这样的高并发情况。

虚拟线程的优势来处理高IO密集型任务的时候比,如如网络请求、文件读写或数据库查询,因为虚拟线程是由JVM来调控,在遇到阻塞的时候会更轻松地切换到另一个虚拟线程来继续工作。

本人在项目中主要是在日志部分,删除redis缓存,一些不影响主线程的任务的时候会使用虚拟线程。

次提交的任务都是创建一个新的虚拟线程去执行,而不是复用之前的线程。那虚拟线程怎么复用呢?

那就需要我们自己实现线程池去管理虚拟线程

现在JDK没有提供复用虚拟线程的方法,我猜可能也是设计虚拟线程的时候就是本着一个虚拟线程一个任务去做的。

我也写了一个demo来循环使用同一个虚拟线程,看看就行。实际使用还是只完成单独一个任务即可

public class demo {

    private static volatile LinkedList<Runnable> workQueue = new LinkedList<>();
    private static int count = 0;


    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, InterruptedException {
        Thread thread1 = Thread.ofVirtual().name("c-vt").start(() -> {
            while (true) {
                boolean interrupted = Thread.interrupted();
                if (interrupted) {
                    break;
                }
                if (!workQueue.isEmpty()) {

                    Runnable runnable = workQueue.removeFirst();
                    runnable.run();

                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        Thread thread2 = Thread.ofVirtual().name("p-vt").start(() -> {
            while (true) {
                boolean interrupted = Thread.interrupted();
                if (interrupted) {
                    break;
                }
                workQueue.addLast(() -> {
                    System.out.println("任务 " + count++ + "queue size = " + workQueue.size());
                });
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        Thread.sleep(10000);
        thread1.interrupt();
        thread2.interrupt();
        Thread.sleep(200);
        System.out.println("main final queue size = "+workQueue.size());


    }

}

四、虚拟线程的使用场景

一下是AI提供的场景

  1. 高并发场景:
  • 在处理大量短生命周期、I/O 密集型任务时,如网络请求、文件读写或数据库查询,虚拟线程特别有用。由于它们不需要消耗操作系统级别的资源(如内核线程),可以轻松创建并同时运行数千甚至数百万个虚拟线程,极大地提高了系统的并发能力,进而提升整体吞吐量。
  1. 响应式编程与异步服务:
  • 在构建响应式应用程序或异步微服务架构中,虚拟线程能够简化异步编程模型。开发者可以使用类似于传统同步编程风格编写代码,但底层通过虚拟线程实现非阻塞执行,避免了复杂的回调和 Future/CompletableFuture 管理,提升了代码可读性和维护性。
  1. 事件驱动框架:
  • 在事件处理器、消息队列消费者等场景中,每当有新事件到达时,通常会启动一个新线程进行处理以保证响应速度。采用虚拟线程,可以显著降低因频繁创建和销毁线程带来的开销,特别是在事件密集且处理逻辑较轻量的情况下。
  1. Web 服务器与API服务:
  • 对于高负载的 Web 服务器和 API 服务,每个请求通常在一个单独的线程上处理。使用虚拟线程,服务器可以轻松应对海量并发连接,而无需担心线程资源耗尽或过度优化线程池大小。这有助于提高服务的可伸缩性和性能。
  1. 大数据处理与流计算:
  • 在处理大规模数据流或批处理作业时,虚拟线程可以帮助分解任务为大量细粒度的工作单元,并行执行这些单元。由于虚拟线程的低成本特性,可以在不增加系统负担的前提下实现近乎无界的并行度,从而加速数据处理流程。
  1. 测试与模拟:
  • 在进行并发测试、压力测试或模拟高并发用户行为时,虚拟线程使得快速生成大量并发线程变得简单且成本低廉。这有助于更准确地评估系统在极端并发条件下的表现,而不必担心实际环境中难以复现这样的高并发情况。

虚拟线程的优势来处理高IO密集型任务的时候比,如如网络请求、文件读写或数据库查询,因为虚拟线程是由JVM来调控,在遇到阻塞的时候会更轻松地切换到另一个虚拟线程来继续工作。

本人在项目中主要是在日志部分,删除redis缓存,一些不影响主线程的任务的时候会使用虚拟线程。

作者水平有限,文章中可能会出现错误,希望各位大佬指正!

  • 10
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值