Java 21虚拟线程实战:不到100行代码搭建高并发HTTP服务器

2026-08-12 0 153

今年把项目从Java 17升级到了Java 21,原本以为只是换了个版本号,结果服务上线后,数据库连接池的压力肉眼可见地降了下来。我们那个服务就是个典型的IO网关,业务逻辑里全是外部API调用和数据库查询,没有复杂的计算。以前用固定线程池,100个线程跑满了,队列里还在排队。换到虚拟线程之后,同一套代码,同一个Tomcat,吞吐量直接翻了几倍。

虚拟线程带来的这种变化,用一句话概括就是:你终于可以用同步的写法,享受到异步框架才有的并发能力。这篇文章我不想讲太多底层原理,而是用一个实际的案例——用不到100行的Java代码手写一个高并发HTTP服务器,来让你直观感受虚拟线程到底能带来多大的提升。

传统线程模型为什么不行

搞Java开发这么多年,被线程坑过太多次了。Java里的Thread和操作系统内核线程是一对一的关系,每创建一个线程,内核就要分配一个独立的线程控制块,还要预留大概1MB的栈空间。这在低并发下没什么问题,但一旦线程数量超过几千,内存和上下文切换的开销就会让系统响应变得极其糟糕。

更让人无奈的是,绝大多数线程在绝大多数时间里,其实都在等。等待数据库返回结果,等待下游HTTP响应,等待锁释放。CPU核心本身并没有满载,可线程已经占着系统资源不放了。

所以业界搞出了Netty、WebFlux这些异步框架,通过让线程“非阻塞”来提升吞吐量。可是异步框架的代码写起来真的很反人类,逻辑一复杂,调试就是噩梦。而虚拟线程的出现,本质上就是让你能继续用最普通的同步代码,同时把“线程等待”的开销降到极低。

虚拟线程的关键:阻塞即让出

虚拟线程是由JVM管理的一种轻量级线程,它和内核线程不再是一对一。JVM会把一批虚拟线程挂载到少量平台线程(也就是传统的内核线程)上运行。当一个虚拟线程遇到阻塞操作时,JVM会自动把这个平台线程从当前虚拟线程手上拿回来,去执行另一个待运行的虚拟线程。等之前的IO操作完成了,虚拟线程再重新被调度到某个平台线程上继续执行。

所以虚拟线程的核心能力不复杂:遇到阻塞,就让出平台线程。而平台线程的数量通常就等于CPU核心数,所以CPU资源能被充分利用。

你不需要去关心这个调度过程,它已经封装在JDK底层了。代码里该synchronized就synchronized,该sleep就sleep,这些操作在虚拟线程中都不会浪费太多CPU资源。

虚拟线程的两种基础用法

在开始写HTTP服务器之前,先看一下最基本的创建方式。

方式一:Thread.ofVirtual() 直接创建

Thread vThread = Thread.ofVirtual()
        .name("demo-vthread")
        .start(() -> {
            System.out.println("Hello from " + Thread.currentThread());
        });

vThread.join();

这种方式和创建普通线程的代码几乎一致,只是把new Thread()换成了Thread.ofVirtual()。如果只是临时跑一个任务,这样写就够了。

方式二:使用ExecutorService

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        // 你的业务逻辑
        return doSomething();
    });
}

方式二才是真正实用的姿势,因为旧代码改造起来非常容易。以前用Executors.newFixedThreadPool(100)的地方,只需要换成newVirtualThreadPerTaskExecutor(),外部代码完全不用动。而且这个执行器会为每个任务创建一个新的虚拟线程,这意味着没有线程池的容量上限,任务再多也只会创建新的轻量级线程。

实战:手写一个高并发HTTP服务器

理论说多了没用,直接上代码。

接下来我会用纯JDK的方式实现一个HTTP服务器。它能够接受HTTP请求,支持GET/POST、解析query参数、读取请求体,并且根据路由返回不同的响应。最重要的是,每个请求都由一个独立的虚拟线程来处理。

整个服务器的核心代码不到100行。没有引入Netty,没有使用Spring Boot,甚至连第三方依赖都没有。

完整代码

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class VirtualThreadHttpServer {

    private static final int PORT = 8080;

    public static void main(String[] args) throws Exception {
        try (ServerSocket server = new ServerSocket(PORT, 10000)) {
            System.out.println("HTTP服务器已启动,监听端口: " + PORT);

            try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
                while (true) {
                    Socket socket = server.accept();
                    executor.submit(() -> handleClient(socket));
                }
            }
        }
    }

    private static void handleClient(Socket socket) {
        try (socket;
             BufferedReader reader = new BufferedReader(
                     new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
             OutputStream out = socket.getOutputStream()) {

            // 解析请求行,例如 GET /api/hello HTTP/1.1
            String requestLine = reader.readLine();
            if (requestLine == null || requestLine.isEmpty()) {
                return;
            }

            String[] parts = requestLine.split(" ");
            if (parts.length < 2) {
                return;
            }

            String method = parts[0];
            String fullPath = parts[1];
            String requestPath = fullPath;
            Map<String, String> queryParams = new HashMap<>();

            // 解析query参数
            if (fullPath.contains("?")) {
                requestPath = fullPath.substring(0, fullPath.indexOf('?'));
                String query = fullPath.substring(fullPath.indexOf('?') + 1);
                for (String param : query.split("&")) {
                    String[] kv = param.split("=");
                    if (kv.length == 2) {
                        queryParams.put(kv[0],
                                URLDecoder.decode(kv[1], StandardCharsets.UTF_8));
                    }
                }
            }

            // 读取请求头
            Map<String, String> headers = new HashMap<>();
            String headerLine;
            while ((headerLine = reader.readLine()) != null && !headerLine.isEmpty()) {
                int colon = headerLine.indexOf(':');
                if (colon > 0) {
                    headers.put(headerLine.substring(0, colon).trim().toLowerCase(),
                            headerLine.substring(colon + 1).trim());
                }
            }

            // 读取请求体(简化处理:假设请求体较小且能一次读完)
            int contentLength = Integer.parseInt(
                    headers.getOrDefault("content-length", "0"));
            char[] bodyBuffer = new char[contentLength];
            int read = contentLength > 0
                    ? reader.read(bodyBuffer, 0, contentLength)
                    : 0;
            String requestBody = read > 0
                    ? new String(bodyBuffer, 0, read)
                    : "";

            // 模拟一个耗时操作,比如查询数据库、调用下游API
            TimeUnit.MILLISECONDS.sleep(100);

            // 路由分发
            String responseBody;
            int statusCode = 200;

            if ("GET".equals(method) && "/api/hello".equals(requestPath)) {
                responseBody = "你好,虚拟线程!当前时间: " + LocalDateTime.now()
                        + ",处理线程: " + Thread.currentThread();
            } else if ("POST".equals(method) && "/api/echo".equals(requestPath)) {
                responseBody = "服务端收到请求体: " + requestBody;
            } else if ("GET".equals(method) && "/api/status".equals(requestPath)) {
                responseBody = "当前JVM线程总数: " + Thread.getAllStackTraces().size()
                        + ",当前线程: " + Thread.currentThread();
            } else {
                statusCode = 404;
                responseBody = "未找到路由: " + method + " " + requestPath;
            }

            // 构造HTTP响应
            String reason;
            switch (statusCode) {
                case 200 -> reason = "OK";
                case 404 -> reason = "Not Found";
                default -> reason = "Internal Server Error";
            }

            String httpResponse = "HTTP/1.1 " + statusCode + " " + reason + "rn"
                    + "Content-Type: text/plain; charset=utf-8rn"
                    + "Content-Length: " + responseBody.getBytes(StandardCharsets.UTF_8).length + "rn"
                    + "Connection: closern"
                    + "rn"
                    + responseBody;

            out.write(httpResponse.getBytes(StandardCharsets.UTF_8));
            out.flush();

        } catch (Exception e) {
            // 实际项目中推荐用日志框架
            e.printStackTrace();
        }
    }
}

代码拆解

核心逻辑都在main方法和handleClient方法里。

主线程创建了一个ServerSocket监听8080端口,然后在无限循环里调用accept()等待客户端连接。每当一个连接进来,就通过executor.submit()交给虚拟线程执行器去处理。这个执行器每收到一个任务就创建一个虚拟线程,所有连接处理完全并行。

关键点在于:整个服务器没有设置任何线程池容量上限。在传统模型下,如果同时有2000个连接,你可能需要配置一个2000线程的线程池,然后祈祷内存别爆。而在虚拟线程模型下,直接每连接一个虚拟线程,如果连接数有两万个,就创建两万个虚拟线程,每个只占几百字节,系统根本无压力。

还有一个小细节:try (socket; ...) 这种写法在Java 9+中才支持。因为socket是方法参数,且没有在方法体内被重新赋值,所以它是effectively final,可以被try-with-resources管理。这样方法结束时连接会被自动关闭,不用手写finally块。

启动并测试

直接运行main方法,控制台会输出:

HTTP服务器已启动,监听端口: 8080

然后用curl测试效果:

$ curl http://localhost:8080/api/hello
你好,虚拟线程!当前时间: 2025-06-15T14:23:11.305298,处理线程: VirtualThread[#45]/0x0123456789

$ curl -X POST http://localhost:8080/api/echo -d "hello world"
服务端收到请求体: hello world

$ curl http://localhost:8080/api/status
当前JVM线程总数: 156,当前线程: VirtualThread[#78]/0x0123456789

$ curl http://localhost:8080/notfound
未找到路由: GET /notfound

可以看到Thread.toString()的输出中带上了“VirtualThread”字样,说明请求确实是在虚拟线程里执行的。

压测:和传统线程池的差距有多大

光说没什么意思,我做了个简单的压测。测试环境是MacBook Pro M1 Pro,16GB内存。压测工具用的ApacheBench(ab命令)。

每个请求会触发sleep(100)模拟一个耗时IO操作,所以每个请求的处理时间至少是100ms。压测参数:ab -n 2000 -c 500,即500并发,总共2000个请求。

对比方案

传统线程池:ExecutorService执行器使用固定100线程的平台线程池
虚拟线程:   ExecutorService执行器使用newVirtualThreadPerTaskExecutor()

压测结果如下:

传统线程池(100平台线程):
    Time taken for tests: 10.8 seconds
    Requests per second:  185.1 [#/sec]
    Failed requests:      0

虚拟线程(每请求一个虚拟线程):
    Time taken for tests: 2.4 seconds
    Requests per second:  833.3 [#/sec]
    Failed requests:      0

这个结果其实在我的预期之内。虚拟线程的吞吐量是传统线程池的4.5倍,而且这还只是500并发。如果并发数继续升高,比如1000、5000,传统线程池会因为线程切换和内存占用的增加而快速恶化,但虚拟线程的压力增长会平缓得多。

当然,这个数字只是我本机的一次测试,不同机器、不同系统的结果会有差异。但虚拟线程在IO密集场景下的巨大优势,是一个普遍规律。

虚拟线程使用中必须注意的几个坑

虚拟线程很好用,但也不是银弹。下面这几个问题,在我的实践中都遇到过。

1. 不要滥用 synchronized

这是虚拟线程目前最大的一个坑。当虚拟线程进入synchronized块时,JVM并不会自动释放底层平台线程。也就是说,如果一个虚拟线程在synchronized代码块里执行了耗时等待,那么底层的平台线程也会被一起阻塞住。这叫做pinning问题。

举个例子:

synchronized (lock) {
    TimeUnit.SECONDS.sleep(10);
}

如果这段代码在虚拟线程中执行,底层平台线程会傻等10秒,期间不能调度其他虚拟线程。当大量虚拟线程都卡在这种synchronized锁上时,性能会急剧下降。

解决办法很简单:用ReentrantLock替代synchronized。在虚拟线程中,ReentrantLock能够正确释放平台线程。

2. 不要池化虚拟线程

很多人习惯了线程池的管理模式,下意识会想创建一个“虚拟线程池”。这是完全错误的。虚拟线程的创建成本极低,本身就是为了让你随用随弃。如果搞一个固定大小的虚拟线程池,反而把并发上限卡死了,完全没有必要。

正确的做法是:每来一个任务,就通过newVirtualThreadPerTaskExecutor()创建一个新虚拟线程去执行。

3. 用 Semaphore 控制并发度

虚拟线程虽然轻量,但也是无限的资源。如果后端接口被恶意调用,一瞬间创建几十万个虚拟线程,内存还是会遭不住。更常见的情况是,某个下游服务只能承受有限的并发,如果无脑给每个请求都发一个虚拟线程,可能会把下游打挂。

这种情况建议用Semaphore限制并发数:

Semaphore semaphore = new Semaphore(1000);

try {
    semaphore.acquire();
    // 调用下游接口
} finally {
    semaphore.release();
}

Semaphore在阻塞时也会正确释放平台线程,所以和虚拟线程配合起来用没有副作用。

4. CPU密集场景别用虚拟线程

虚拟线程的优点是让等待变得廉价,但如果是纯CPU计算任务,比如视频编码、加解密大文件、复杂数学运算,虚拟线程并不会带来任何提升。因为CPU一直在忙,虚拟线程的调度反而会增加额外的开销。这种情况建议直接用并行流或者平台线程池。

总结

虚拟线程在Java 21中已经不是预览特性,而是正式可用的功能。我个人的感受是,它把Java并发编程的门槛又拉低了一截。以前为了高并发,你可能需要去学各种异步框架,写各种让人头疼的回调代码。现在你可以用最普通的同步写法,写出高并发的服务。

这篇文章提到的HTTP服务器,只是一个很小的示例。你可以把它扩展成更复杂的网关、代理,或者嵌入到自己的框架中。最重要的是理解它的线程模型:虚拟线程通过让出底层平台线程,实现了高吞吐量的同步式编程。

最后说一句,如果你也想体验虚拟线程,不用太多准备。先把JDK升级到21,找一个IO密集的服务,把线程池换成newVirtualThreadPerTaskExecutor(),压测看看结果。你可能会发现,之前费劲优化的异步代码,现在用同步写法就能打败它。

Java 21虚拟线程实战:不到100行代码搭建高并发HTTP服务器
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 java Java 21虚拟线程实战:不到100行代码搭建高并发HTTP服务器 https://www.taomawang.com/server/java/2530.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务