从 JDK9 到 JDK21:并发编程的范式革命与虚拟线程终极指南

0 阅读21分钟

引言

Java并发编程自诞生以来,一直是开发者面临的最大挑战之一。从最初的Thread类和Runnable接口,到JDK5引入的java.util.concurrent包,再到JDK8的CompletableFuture和Stream API,Java一直在不断演进以满足现代应用对高并发的需求。

JDK9到JDK21的发布,标志着Java并发编程进入了一个全新的时代。特别是JDK21中正式转正的虚拟线程(Virtual Thread),彻底改变了Java并发编程的范式,解决了困扰Java开发者多年的平台线程资源瓶颈问题。

一、JDK9-JDK21并发编程核心新特性全景

1.1 JDK9:并发编程的基础重构

JDK9是Java模块化系统的第一个版本,同时也对并发编程的基础进行了大量重构和优化。

1.1.1 CompletableFuture的增强

JDK9为CompletableFuture增加了9个新方法,极大地提升了其表达能力和灵活性。

package com.jam.demo.concurrent;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

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

/**
 * JDK9 CompletableFuture增强示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class CompletableFutureEnhanceDemo {
    private static final ExecutorService executor = Executors.newFixedThreadPool(4);

    /**
     * 演示completeOnTimeout方法
     * 如果在指定时间内没有完成,使用默认值完成
     */
    public static void demoCompleteOnTimeout() {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                return "实际结果";
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return "中断结果";
            }
        }, executor);

        String result = future.completeOnTimeout("超时默认值"1, TimeUnit.SECONDS)
                .join();
        log.info("completeOnTimeout结果: {}", result);
    }

    /**
     * 演示orTimeout方法
     * 如果在指定时间内没有完成,抛出TimeoutException
     */
    public static void demoOrTimeout() {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                return "实际结果";
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return "中断结果";
            }
        }, executor);

        try {
            future.orTimeout(1, TimeUnit.SECONDS).join();
        } catch (Exception e) {
            log.error("orTimeout抛出异常", e);
        }
    }

    /**
     * 演示failedFuture方法
     * 创建一个已经异常完成的CompletableFuture
     */
    public static void demoFailedFuture() {
        CompletableFuture<String> future = CompletableFuture.failedFuture(
                new RuntimeException("预定义异常"));

        try {
            future.join();
        } catch (Exception e) {
            log.error("failedFuture抛出异常", e);
        }
    }

    /**
     * 演示delayedExecutor方法
     * 创建一个延迟执行任务的Executor
     */
    public static void demoDelayedExecutor() {
        CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS, executor)
                .execute(() -> log.info("延迟1秒执行的任务"));
    }

    public static void main(String[] args) {
        demoCompleteOnTimeout();
        demoOrTimeout();
        demoFailedFuture();
        demoDelayedExecutor();

        executor.shutdown();
    }
}

1.1.2 Flow API:响应式编程的标准

JDK9引入了java.util.concurrent.Flow接口,定义了响应式流(Reactive Streams)的标准规范。这是Java官方对响应式编程的首次支持,为后续Spring WebFlux等框架提供了统一的基础。

package com.jam.demo.concurrent;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.TimeUnit;

/**
 * JDK9 Flow API示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class FlowApiDemo {
    /**
     * 自定义订阅者
     */
    static class SimpleSubscriber<T> implements Flow.Subscriber<T> {
        private Flow.Subscription subscription;
        private final String name;

        public SimpleSubscriber(String name) {
            this.name = name;
        }

        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            this.subscription = subscription;
            log.info("{}: 订阅成功", name);
            subscription.request(1);
        }

        @Override
        public void onNext(T item) {
            log.info("{}: 收到消息: {}", name, item);
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            subscription.request(1);
        }

        @Override
        public void onError(Throwable throwable) {
            log.error("{}: 发生错误", name, throwable);
        }

        @Override
        public void onComplete() {
            log.info("{}: 数据流结束", name);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        try (SubmissionPublisher<String> publisher = new SubmissionPublisher<>()) {
            publisher.subscribe(new SimpleSubscriber<>("订阅者1"));
            publisher.subscribe(new SimpleSubscriber<>("订阅者2"));

            for (int i = 1; i <= 5; i++) {
                publisher.submit("消息-" + i);
                log.info("发布消息: 消息-{}", i);
            }
        }

        TimeUnit.SECONDS.sleep(1);
    }
}

1.1.3 VarHandle:变量句柄

VarHandle提供了对变量的细粒度原子操作支持,替代了Unsafe类的大部分功能。它支持对对象字段、数组元素的各种原子操作,包括get、set、compareAndSet、getAndAdd等。

package com.jam.demo.concurrent;

import lombok.extern.slf4j.Slf4j;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.CountDownLatch;

/**
 * JDK9 VarHandle示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class VarHandleDemo {
    private volatile int count = 0;
    private static final VarHandle COUNT_HANDLE;

    static {
        try {
            COUNT_HANDLE = MethodHandles.lookup()
                    .findVarHandle(VarHandleDemo.class"count"int.class);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 使用VarHandle进行原子递增
     */
    public void increment() {
        COUNT_HANDLE.getAndAdd(this1);
    }

    /**
     * 使用VarHandle进行CAS操作
     */
    public boolean compareAndSet(int expect, int update) {
        return COUNT_HANDLE.compareAndSet(this, expect, update);
    }

    public static void main(String[] args) throws InterruptedException {
        VarHandleDemo demo = new VarHandleDemo();
        CountDownLatch latch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    demo.increment();
                }
                latch.countDown();
            }).start();
        }

        latch.await();
        log.info("最终计数: {}", demo.count);

        boolean success = demo.compareAndSet(1000020000);
        log.info("CAS操作结果: {}, 计数: {}", success, demo.count);
    }
}

1.2 JDK10:局部变量类型推断

JDK10引入了局部变量类型推断(var关键字),虽然这不是专门为并发编程设计的特性,但它极大地简化了并发代码的编写,提高了代码的可读性。

package com.jam.demo.concurrent;

import lombok.extern.slf4j.Slf4j;

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

/**
 * JDK10 var关键字在并发编程中的应用
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class VarInConcurrentDemo {
    public static void main(String[] args) {
        var executor = Executors.newFixedThreadPool(4);

        var future1 = CompletableFuture.supplyAsync(() -> "Hello", executor);
        var future2 = CompletableFuture.supplyAsync(() -> "World", executor);

        var combinedFuture = future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2);

        combinedFuture.thenAccept(result -> log.info("结果: {}", result));

        executor.shutdown();
    }
}

1.3 JDK11:HttpClient API

JDK11将HttpClient API正式转正,替代了老旧的HttpURLConnection。新的HttpClient完全支持异步非阻塞操作,基于CompletableFuture实现,非常适合高并发的网络请求场景。

package com.jam.demo.concurrent;

import lombok.extern.slf4j.Slf4j;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * JDK11 HttpClient API示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class HttpClientDemo {
    private static final HttpClient httpClient = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_2)
            .connectTimeout(Duration.ofSeconds(10))
            .build();

    /**
     * 同步GET请求
     */
    public static void syncGet() throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/get"))
                .timeout(Duration.ofSeconds(5))
                .header("Accept""application/json")
                .GET()
                .build();

        HttpResponse<String> response = httpClient.send(request,
                HttpResponse.BodyHandlers.ofString());

        log.info("同步请求状态码: {}", response.statusCode());
        log.info("同步请求响应: {}", response.body());
    }

    /**
     * 异步GET请求
     */
    public static CompletableFuture<StringasyncGet(String url) {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .timeout(Duration.ofSeconds(5))
                .header("Accept""application/json")
                .GET()
                .build();

        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::body);
    }

    /**
     * 批量异步请求
     */
    public static void batchAsyncRequests() {
        List<CompletableFuture<String>> futures = IntStream.range(16)
                .mapToObj(i -> asyncGet("https://httpbin.org/get?param=" + i))
                .collect(Collectors.toList());

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
                .thenRun(() -> {
                    List<String> results = futures.stream()
                            .map(CompletableFuture::join)
                            .collect(Collectors.toList());
                    log.info("批量请求完成,共收到{}个响应", results.size());
                })
                .join();
    }

    public static void main(String[] args) throws Exception {
        syncGet();
        batchAsyncRequests();
    }
}

1.4 JDK14:Record类

JDK14引入了Record类,用于创建不可变的数据载体。在并发编程中,不可变对象是线程安全的,因此Record类非常适合在多线程环境中传递数据。

package com.jam.demo.concurrent;

import lombok.extern.slf4j.Slf4j;

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

/**
 * JDK14 Record类在并发编程中的应用
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class RecordInConcurrentDemo {
    /**
     * 用户信息Record,不可变,线程安全
     */
    public record User(Long id, String username, String email) {}

    /**
     * 异步获取用户信息
     */
    public static CompletableFuture<User> getUserById(Long userId) {
        return CompletableFuture.supplyAsync(() -> {
            // 模拟数据库查询
            return new User(userId, "user" + userId, "user" + userId + "@example.com");
        });
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(4);

        CompletableFuture.allOf(
                getUserById(1L).thenAccept(user -> log.info("用户1: {}", user)),
                getUserById(2L).thenAccept(user -> log.info("用户2: {}", user)),
                getUserById(3L).thenAccept(user -> log.info("用户3: {}", user))
        ).join();

        executor.shutdown();
    }
}

1.5 JDK16:Stream API增强

JDK16对Stream API进行了增强,增加了toList()、mapMulti()等方法,简化了并发流的操作。

package com.jam.demo.concurrent;

import lombok.extern.slf4j.Slf4j;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * JDK16 Stream API增强示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class StreamEnhanceDemo {
    /**
     * 演示toList()方法
     * 返回不可变列表
     */
    public static void demoToList() {
        List<Integer> list = IntStream.range(110)
                .parallel()
                .filter(i -> i % 2 == 0)
                .boxed()
                .toList();

        log.info("toList结果: {}", list);
    }

    /**
     * 演示mapMulti()方法
     * 替代flatMap,性能更好
     */
    public static void demoMapMulti() {
        List<Integer> list = IntStream.range(15)
                .parallel()
                .<Integer>mapMulti((num, consumer) -> {
                    consumer.accept(num);
                    consumer.accept(num * 2);
                })
                .boxed()
                .collect(Collectors.toList());

        log.info("mapMulti结果: {}", list);
    }

    public static void main(String[] args) {
        demoToList();
        demoMapMulti();
    }
}

1.6 JDK17:ZGC正式版

JDK17将ZGC垃圾回收器正式转正。ZGC是一款低延迟垃圾回收器,其暂停时间不超过10毫秒,并且不会随着堆大小的增加而增加。这对于高并发、低延迟要求的应用来说是一个巨大的福音。

ZGC的主要特点:

  • 亚毫秒级暂停时间
  • 支持TB级堆内存
  • 并发执行大部分GC工作
  • 对应用吞吐量影响小

1.7 JDK19:虚拟线程预览版

JDK19引入了虚拟线程的第一个预览版,这是Java并发编程史上最重要的变革之一。虚拟线程是轻量级的线程,由JVM管理,而不是操作系统。一个平台线程可以运行成千上万个虚拟线程,极大地提高了并发能力。

1.8 JDK20:虚拟线程第二次预览

JDK20对虚拟线程进行了改进和优化,解决了一些在第一个预览版中发现的问题,并增加了一些新的API。

1.9 JDK21:虚拟线程正式版

JDK21将虚拟线程正式转正,标志着Java并发编程进入了一个全新的时代。同时,JDK21还引入了结构化并发(Structured Concurrency)的预览版,进一步简化了并发编程的复杂性。

二、虚拟线程深度解析

2.1 什么是虚拟线程

虚拟线程是JVM管理的轻量级线程,也被称为"用户态线程"或"纤程"。与平台线程(Platform Thread)不同,虚拟线程不是操作系统内核线程的一对一映射,而是由JVM在用户态进行调度和管理。

一个平台线程可以承载成千上万个虚拟线程的执行。当虚拟线程执行阻塞操作时,JVM会自动将其从平台线程上卸载,让平台线程去执行其他虚拟线程。这样就避免了平台线程因阻塞而浪费资源的问题。

2.2 平台线程vs虚拟线程

平台线程和虚拟线程的核心区别如下表所示:

特性平台线程虚拟线程
调度者操作系统内核JVM
上下文切换内核态切换,开销大用户态切换,开销小
内存占用每个线程栈默认1MB初始栈大小几百字节
数量限制通常几千个可以达到百万级
创建成本极低
阻塞行为阻塞整个平台线程仅阻塞虚拟线程
池化必要性必须池化不需要池化

2.3 虚拟线程的底层实现原理

2.3.1 调度模型

虚拟线程采用了M:N的调度模型,即M个虚拟线程映射到N个平台线程上执行。JDK中的虚拟线程实现基于ForkJoinPool,默认使用一个大小等于CPU核心数的ForkJoinPool作为调度器。

当虚拟线程需要执行时,它会被提交到调度器的任务队列中。调度器会将任务分配给空闲的平台线程执行。当虚拟线程执行阻塞操作时,JVM会保存其执行上下文,并将平台线程释放出来执行其他虚拟线程。当阻塞操作完成后,虚拟线程会被重新提交到调度器的任务队列中,等待再次被调度执行。

2.3.2 栈管理

虚拟线程的栈是分段式的,由多个栈帧块组成。每个栈帧块的大小通常是4KB。当虚拟线程的栈需要增长时,JVM会动态分配新的栈帧块。当栈收缩时,JVM会回收不再使用的栈帧块。

这种分段式栈的设计使得虚拟线程的初始内存占用非常小,只有几百字节。同时,它也支持栈的动态增长和收缩,提高了内存利用率。

2.3.3 阻塞处理

这是虚拟线程最核心的特性。当虚拟线程执行以下阻塞操作时,JVM会自动进行挂载和卸载:

  • Thread.sleep()
  • Object.wait()
  • LockSupport.park()
  • java.util.concurrent.locks.Lock.lock()
  • java.nio.channels.InterruptibleChannel的IO操作
  • java.net.Socket的IO操作

当虚拟线程执行这些阻塞操作时,JVM会:

  1. 保存虚拟线程的执行上下文
  2. 将虚拟线程从平台线程上卸载
  3. 让平台线程去执行其他虚拟线程
  4. 当阻塞操作完成后,将虚拟线程重新提交到调度器
  5. 调度器会将虚拟线程分配给某个平台线程继续执行

需要注意的是,并不是所有的阻塞操作都会触发虚拟线程的卸载。如果阻塞操作是在本地方法中执行的,或者是通过JNI调用的C代码中的阻塞操作,那么JVM无法拦截,此时会阻塞整个平台线程。

2.3.4 上下文切换

虚拟线程的上下文切换是在用户态进行的,不需要进入内核态。这使得虚拟线程的上下文切换开销比平台线程小得多。

平台线程的上下文切换需要保存和恢复CPU的所有寄存器状态,并且需要切换页表,这些操作都需要在内核态完成,开销很大。而虚拟线程的上下文切换只需要保存和恢复虚拟线程自己的执行上下文,这些操作都在用户态完成,开销很小。

2.4 虚拟线程的生命周期

虚拟线程的生命周期与平台线程类似,包括以下几个状态:

  • NEW:新建状态,线程已创建但未启动
  • RUNNABLE:可运行状态,正在执行或等待CPU时间
  • BLOCKED:阻塞状态,等待获取锁
  • WAITING:等待状态,等待其他线程的通知
  • TIMED_WAITING:计时等待状态
  • TERMINATED:终止状态,线程已执行完毕

三、虚拟线程实战

3.1 创建虚拟线程

JDK21提供了多种创建虚拟线程的方式:

package com.jam.demo.virtual;

import lombok.extern.slf4j.Slf4j;

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

/**
 * 虚拟线程创建示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class VirtualThreadCreationDemo {
    public static void main(String[] args) throws InterruptedException {
        // 方式1:使用Thread.startVirtualThread()
        Thread vThread1 = Thread.startVirtualThread(() -> {
            log.info("虚拟线程1正在执行");
        });
        vThread1.join();

        // 方式2:使用Thread.Builder
        Thread vThread2 = Thread.ofVirtual()
                .name("virtual-thread-2")
                .unstarted(() -> {
                    log.info("虚拟线程2正在执行");
                });
        vThread2.start();
        vThread2.join();

        // 方式3:创建线程工厂
        ThreadFactory factory = Thread.ofVirtual()
                .name("virtual-thread-", 3)
                .factory();

        Thread vThread3 = factory.newThread(() -> {
            log.info("虚拟线程3正在执行");
        });
        vThread3.start();
        vThread3.join();

        // 方式4:使用Executors.newVirtualThreadPerTaskExecutor()
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 4; i <= 6; i++) {
                int finalI = i;
                executor.submit(() -> {
                    log.info("虚拟线程{}正在执行", finalI);
                });
            }
        }
    }
}

3.2 虚拟线程的基本使用

下面是一个简单的虚拟线程使用示例,展示了如何使用虚拟线程执行大量并发任务:

package com.jam.demo.virtual;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 虚拟线程基本使用示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class VirtualThreadBasicDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger counter = new AtomicInteger(0);
        long startTime = System.currentTimeMillis();

        // 创建10000个虚拟线程
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10000; i++) {
                executor.submit(() -> {
                    try {
                        // 模拟IO操作
                        TimeUnit.MILLISECONDS.sleep(10);
                        counter.incrementAndGet();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }

        long endTime = System.currentTimeMillis();
        log.info("执行完成,共处理{}个任务,耗时{}ms",
                counter.get(), endTime - startTime);
    }
}

3.3 虚拟线程与平台线程性能对比

下面我们通过一个简单的测试来对比虚拟线程和平台线程的性能:

package com.jam.demo.virtual;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 虚拟线程与平台线程性能对比
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class ThreadPerformanceComparison {
    private static final int TASK_COUNT = 10000;
    private static final int SLEEP_TIME = 10;

    /**
     * 使用平台线程执行任务
     */
    public static void testPlatformThreads() throws InterruptedException {
        AtomicInteger counter = new AtomicInteger(0);
        long startTime = System.currentTimeMillis();

        // 使用固定大小的线程池
        try (ExecutorService executor = Executors.newFixedThreadPool(200)) {
            for (int i = 0; i < TASK_COUNT; i++) {
                executor.submit(() -> {
                    try {
                        TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
                        counter.incrementAndGet();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }

        long endTime = System.currentTimeMillis();
        log.info("平台线程: 处理{}个任务,耗时{}ms",
                counter.get(), endTime - startTime);
    }

    /**
     * 使用虚拟线程执行任务
     */
    public static void testVirtualThreads() throws InterruptedException {
        AtomicInteger counter = new AtomicInteger(0);
        long startTime = System.currentTimeMillis();

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < TASK_COUNT; i++) {
                executor.submit(() -> {
                    try {
                        TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
                        counter.incrementAndGet();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }

        long endTime = System.currentTimeMillis();
        log.info("虚拟线程: 处理{}个任务,耗时{}ms",
                counter.get(), endTime - startTime);
    }

    public static void main(String[] args) throws InterruptedException {
        // 预热
        testVirtualThreads();
        testPlatformThreads();

        // 正式测试
        testPlatformThreads();
        testVirtualThreads();
    }
}

在典型的测试环境中,虚拟线程的执行速度会比平台线程快5-10倍。这是因为平台线程池的大小有限,大部分时间都在等待线程空闲,而虚拟线程可以同时执行所有任务。

3.4 虚拟线程中的锁

虚拟线程支持所有Java的锁机制,包括synchronized关键字和java.util.concurrent.locks包中的锁。但是,在虚拟线程中使用synchronized关键字会有一些特殊的行为。

当虚拟线程持有synchronized锁时,它不会被卸载。也就是说,如果一个虚拟线程在持有synchronized锁的情况下执行了阻塞操作,那么它会阻塞整个平台线程。这是因为synchronized锁是基于操作系统的监视器实现的,JVM无法拦截这种情况下的阻塞。

为了解决这个问题,建议在虚拟线程中使用java.util.concurrent.locks.ReentrantLock代替synchronized关键字。ReentrantLock是在Java层面实现的,JVM可以拦截其阻塞操作,并正确地卸载虚拟线程。

package com.jam.demo.virtual;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 虚拟线程中的锁使用示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class VirtualThreadLockDemo {
    private static final Lock reentrantLock = new ReentrantLock();
    private static final Object syncLock = new Object();
    private static int counter = 0;

    /**
     * 使用ReentrantLock,推荐在虚拟线程中使用
     */
    public static void incrementWithReentrantLock() {
        reentrantLock.lock();
        try {
            counter++;
            TimeUnit.MILLISECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            reentrantLock.unlock();
        }
    }

    /**
     * 使用synchronized,不推荐在虚拟线程中使用
     */
    public static void incrementWithSync() {
        synchronized (syncLock) {
            counter++;
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        counter = 0;
        long startTime = System.currentTimeMillis();

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10000; i++) {
                executor.submit(VirtualThreadLockDemo::incrementWithReentrantLock);
            }
        }

        long endTime = System.currentTimeMillis();
        log.info("使用ReentrantLock: 计数={}, 耗时={}ms", counter, endTime - startTime);

        counter = 0;
        startTime = System.currentTimeMillis();

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10000; i++) {
                executor.submit(VirtualThreadLockDemo::incrementWithSync);
            }
        }

        endTime = System.currentTimeMillis();
        log.info("使用synchronized: 计数={}, 耗时={}ms", counter, endTime - startTime);
    }
}

3.5 虚拟线程中的ThreadLocal

虚拟线程支持ThreadLocal,并且在虚拟线程中使用ThreadLocal比在平台线程中更加高效。这是因为虚拟线程的ThreadLocal是存储在虚拟线程对象本身中的,而不是存储在一个全局的映射表中。

但是,由于虚拟线程的数量可以非常大,使用ThreadLocal时需要注意内存泄漏问题。如果虚拟线程长时间存在,并且ThreadLocal中存储了大量数据,那么可能会导致内存溢出。

package com.jam.demo.virtual;

import lombok.extern.slf4j.Slf4j;

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

/**
 * 虚拟线程中的ThreadLocal使用示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class VirtualThreadThreadLocalDemo {
    private static final ThreadLocal<String> USER_CONTEXT = new ThreadLocal<>();

    /**
     * 设置用户上下文
     */
    public static void setUserContext(String username) {
        USER_CONTEXT.set(username);
    }

    /**
     * 获取用户上下文
     */
    public static String getUserContext() {
        return USER_CONTEXT.get();
    }

    /**
     * 清除用户上下文
     */
    public static void clearUserContext() {
        USER_CONTEXT.remove();
    }

    public static void main(String[] args) {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 1; i <= 5; i++) {
                int finalI = i;
                executor.submit(() -> {
                    try {
                        setUserContext("user" + finalI);
                        log.info("虚拟线程{}: 用户上下文={}",
                                finalI, getUserContext());
                    } finally {
                        clearUserContext();
                    }
                });
            }
        }
    }
}

四、虚拟线程的适用场景与不适用场景

4.1 适用场景

虚拟线程特别适合以下场景:

  1. IO密集型应用

    • Web应用服务器
    • 微服务
    • 数据库访问
    • 网络通信
    • 文件操作

    在这些场景中,大部分时间都在等待IO操作完成。使用虚拟线程可以同时处理大量的请求,而不会因为平台线程数量的限制而导致性能瓶颈。

  2. 高并发的任务处理

    • 消息队列消费者
    • 批处理任务
    • 定时任务
    • 爬虫

    这些场景通常需要同时处理大量的任务,使用虚拟线程可以极大地提高系统的吞吐量。

  3. 简化并发编程

    • 替代复杂的异步编程模型
    • 简化错误处理
    • 提高代码的可读性和可维护性

    虚拟线程允许开发者使用同步的编程风格来编写异步的代码,避免了回调地狱和CompletableFuture的复杂性。

4.2 不适用场景

虚拟线程不适合以下场景:

  1. CPU密集型应用

    • 科学计算
    • 图像处理
    • 视频编码

    在这些场景中,CPU是瓶颈,增加线程数量并不能提高性能。使用虚拟线程反而会增加调度开销。

  2. 长时间持有synchronized锁的场景 如前所述,当虚拟线程持有synchronized锁时,它不会被卸载。如果长时间持有synchronized锁,会导致平台线程被阻塞,降低系统的并发能力。

  3. 执行本地方法或JNI调用的场景 如果阻塞操作是在本地方法中执行的,JVM无法拦截,会阻塞整个平台线程。

  4. 需要精确控制线程优先级的场景 虚拟线程不支持线程优先级,所有虚拟线程的优先级都是相同的。

五、生产环境落地避坑方案

5.1 不要池化虚拟线程

这是使用虚拟线程最常见的错误。平台线程因为创建成本高,所以需要池化。但是虚拟线程的创建成本非常低,池化虚拟线程不仅没有必要,反而会带来很多问题。

错误的做法:

// 错误:不要池化虚拟线程
ExecutorService executor = new ThreadPoolExecutor(
        1001000L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>(),
        Thread.ofVirtual().factory()
);

正确的做法:

// 正确:为每个任务创建一个新的虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // 提交任务
}

5.2 避免长时间持有synchronized锁

如前所述,在虚拟线程中长时间持有synchronized锁会导致平台线程被阻塞。建议使用ReentrantLock代替synchronized关键字。

如果必须使用synchronized关键字,应该尽量缩小锁的范围,避免在持有锁的情况下执行阻塞操作。

5.3 注意ThreadLocal的使用

由于虚拟线程的数量可以非常大,使用ThreadLocal时需要注意内存泄漏问题。应该在使用完ThreadLocal后及时调用remove()方法清除数据。

另外,不要在ThreadLocal中存储大量数据,否则可能会导致内存溢出。

5.4 避免在虚拟线程中执行CPU密集型任务

虚拟线程适合执行IO密集型任务,不适合执行CPU密集型任务。如果有CPU密集型任务,应该使用专门的平台线程池来执行。

package com.jam.demo.virtual;

import lombok.extern.slf4j.Slf4j;

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

/**
 * CPU密集型任务处理示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class CpuIntensiveTaskDemo {
    // 专门用于执行CPU密集型任务的平台线程池
    private static final ExecutorService CPU_EXECUTOR =
            Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    /**
     * CPU密集型任务
     */
    public static long cpuIntensiveTask(long n) {
        long result = 0;
        for (long i = 0; i < n; i++) {
            result += i;
        }
        return result;
    }

    public static void main(String[] args) {
        try (ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10; i++) {
                virtualExecutor.submit(() -> {
                    // IO操作
                    log.info("执行IO操作");

                    // 将CPU密集型任务提交给专门的线程池
                    long result = CPU_EXECUTOR.submit(() -> cpuIntensiveTask(100000000))
                            .get();

                    log.info("CPU密集型任务结果: {}", result);
                });
            }
        }

        CPU_EXECUTOR.shutdown();
    }
}

5.5 配置合适的调度器参数

虚拟线程的默认调度器是一个大小等于CPU核心数的ForkJoinPool。在大多数情况下,这个默认配置是合适的。但是在某些特殊情况下,可能需要调整调度器的参数。

可以通过以下系统属性来配置虚拟线程的调度器:

  • jdk.virtualThreadScheduler.parallelism:调度器的并行度,默认等于CPU核心数
  • jdk.virtualThreadScheduler.maxPoolSize:调度器的最大线程数,默认是256
  • jdk.virtualThreadScheduler.minRunnable:调度器保持的最小可运行线程数,默认是1

5.6 监控和调优

在生产环境中使用虚拟线程时,需要进行适当的监控和调优。可以使用JDK自带的工具如jconsole、jvisualvm来监控虚拟线程的运行情况。

另外,JDK21提供了一些新的JMX指标来监控虚拟线程:

  • java.lang:type=VirtualThread:虚拟线程的总数
  • java.lang:type=VirtualThreadScheduler:调度器的运行情况

5.7 兼容性问题

在将现有应用迁移到虚拟线程时,需要注意兼容性问题。一些旧的库可能假设线程是平台线程,并且依赖于平台线程的某些特性。

特别是以下情况需要特别注意:

  • 依赖于线程ID的库
  • 依赖于线程优先级的库
  • 依赖于线程组的库
  • 使用ThreadLocal作为缓存的库

六、结构化并发

JDK21引入了结构化并发(Structured Concurrency)的预览版,这是一种新的并发编程范式,旨在简化并发编程的复杂性,并提高代码的可靠性。

结构化并发的核心思想是:并发任务的生命周期应该与代码块的生命周期一致。当代码块执行完毕时,所有在该代码块中启动的并发任务都应该已经完成。

package com.jam.demo.virtual;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.TimeUnit;

/**
 * 结构化并发示例
 *
 * @author ken
 * @date 2026-04-16
 */
@Slf4j
public class StructuredConcurrencyDemo {
    /**
     * 模拟用户服务
     */
    public static String getUser(Long userId) throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(100);
        return "User:" + userId;
    }

    /**
     * 模拟订单服务
     */
    public static String getOrder(Long orderId) throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(150);
        return "Order:" + orderId;
    }

    /**
     * 使用结构化并发同时获取用户和订单信息
     */
    public static void fetchUserAndOrder(Long userId, Long orderId) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            var userFuture = scope.fork(() -> getUser(userId));
            var orderFuture = scope.fork(() -> getOrder(orderId));

            scope.join().throwIfFailed();

            String user = userFuture.get();
            String order = orderFuture.get();

            log.info("用户: {}, 订单: {}", user, order);
        }
    }

    public static void main(String[] args) throws Exception {
        fetchUserAndOrder(1L100L);
    }
}

结构化并发的主要优点:

  • 自动错误传播:如果任何一个任务失败,所有其他任务都会被取消
  • 自动取消:当代码块执行完毕时,所有未完成的任务都会被取消
  • 简化错误处理:不需要手动处理多个CompletableFuture的异常
  • 提高代码的可读性和可维护性

七、总结与展望

JDK9到JDK21的发布,为Java并发编程带来了革命性的变化。特别是虚拟线程的正式转正,彻底解决了困扰Java开发者多年的平台线程资源瓶颈问题。虚拟线程允许开发者使用同步的编程风格来编写高并发的应用,极大地简化了并发编程的复杂性。同时,它也提供了比传统平台线程高得多的性能和吞吐量。但是,虚拟线程并有自己的适用场景和局限性。在使用虚拟线程时,需要注意避免一些常见的陷阱,如池化虚拟线程、长时间持有synchronized锁等。