Java 中的 HTTP 通信格局发生了翻天覆地的变化。以前我们做 HTTP 请求,要么用 Apache HttpClient,要么用 OkHttp,这些第三方库虽然好用,但总得引入依赖。现在不一样了,随着 Java 11 引入标准化的 HttpClient API 和 Java 21 中具有开创性的虚拟线程(Project Loom),Java 现在提供了一个高性能的 HTTP 通信解决方案,足以与任何第三方库相媲美。
当你将现代的 JDK HttpClient 与虚拟线程结合时,你将解锁构建响应式、可扩展应用程序的前所未有效率。这不仅仅是渐进式的改进——这是 Java 处理并发 HTTP 操作的根本性转变。简单来说,以前你要么写复杂的异步代码,要么忍受线程资源限制,现在虚拟线程让你鱼和熊掌兼得。
作为 Java 11 的标准功能引入的 java.net.http.HttpClient 是为现代应用程序需求从头开始构建的。与传统的 HttpURLConnection 不同,这个新客户端默认支持 HTTP/2,自动处理连接池,提供同步和异步 API,并与 Java 的响应式流无缝集成。简单来说,HttpURLConnection 就像老式的座机电话,功能单一,用起来麻烦;而新的 HttpClient 就像智能手机,功能强大,用起来顺手。
API 清晰、流畅且直观,看个例子就明白了:
// 创建 HTTP 客户端,配置 HTTP/2 版本和连接超时
// Create HTTP client with HTTP/2 version and connection timeout
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // 使用 HTTP/2 协议 / Use HTTP/2 protocol
.connectTimeout(Duration.ofSeconds(10)) // 设置连接超时为 10 秒 / Set connection timeout to 10 seconds
.build();
// 构建 HTTP 请求,设置 URI 和请求头
// Build HTTP request with URI and headers
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users")) // 设置请求地址 / Set request URL
.header("Accept", "application/json") // 设置 Accept 请求头 / Set Accept header
.GET() // 设置为 GET 请求 / Set as GET request
.build();
// 发送请求并获取响应,响应体为字符串类型
// Send request and get response with string body
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
虚拟线程是 Java 21 中 Project Loom 的旗舰功能,从根本上改变了基于线程的并发经济学。传统的平台线程成本高昂——每个线程消耗大量内存(通常为 1-2 MB),并且需要操作系统级别的上下文切换。这限制了应用程序最多只能使用数千个线程。想象一下,如果你的应用需要处理 10 万个并发请求,用传统线程的话,光内存就要吃掉 100-200 GB,这显然不现实。
虚拟线程是轻量级的用户模式线程,由 JVM 而非操作系统管理。你可以轻松创建数百万个虚拟线程,每个线程只占用几 KB 内存。它们非常适合像 HTTP 调用这样的 I/O 密集型操作,因为线程大部分时间都在等待网络响应,而不是真正在执行计算。这就像传统线程是雇佣全职员工,成本高;虚拟线程是雇佣临时工,按需分配,成本低。
// 轻松创建数百万个虚拟线程
// Easily create millions of virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 创建 100 万个虚拟线程任务
// Create 1 million virtual thread tasks
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
// 进行 HTTP 调用,每个虚拟线程执行一个任务
// Perform HTTP call, each virtual thread executes one task
return fetchUserData(i);
});
});
}
当将 JDK HttpClient 与虚拟线程集成时,魔法就发生了。这种组合让你获得了同步代码的简洁性以及异步操作的可扩展性。以前你要么写复杂的异步回调,要么忍受线程资源限制,现在你可以用同步的方式写代码,但获得异步的性能。
下面我们来看一个利用虚拟线程进行并发 HTTP 请求的实际实现,这个例子展示了如何同时请求多个 URL:
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
/**
* 基于虚拟线程的 HTTP 客户端
* HTTP client based on virtual threads
*/
public class VirtualThreadHttpClient {
// HTTP 客户端实例
// HTTP client instance
private final HttpClient httpClient;
/**
* 构造函数,初始化使用虚拟线程的 HTTP 客户端
* Constructor, initialize HTTP client using virtual threads
*/
public VirtualThreadHttpClient() {
this.httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // 使用 HTTP/2 协议 / Use HTTP/2 protocol
.connectTimeout(Duration.ofSeconds(10)) // 设置连接超时 / Set connection timeout
.executor(Executors.newVirtualThreadPerTaskExecutor()) // 使用虚拟线程执行器 / Use virtual thread executor
.build();
}
/**
* 并发获取多个 URL 的内容
* Fetch content from multiple URLs concurrently
* @param urls URL 列表 / List of URLs
* @return 响应内容列表 / List of response contents
*/
public List<String> fetchMultipleUrls(List<String> urls) {
// 创建虚拟线程执行器,自动管理资源
// Create virtual thread executor with automatic resource management
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 为每个 URL 提交一个虚拟线程任务
// Submit a virtual thread task for each URL
var futures = urls.stream()
.map(url -> executor.submit(() -> fetchUrl(url)))
.toList();
// 等待所有任务完成并收集结果
// Wait for all tasks to complete and collect results
return futures.stream()
.map(future -> {
try {
return future.get(); // 获取任务结果 / Get task result
} catch (Exception e) {
return "Error: FunTester - " + e.getMessage(); // 返回错误信息 / Return error message
}
})
.toList();
}
}
/**
* 获取单个 URL 的内容
* Fetch content from a single URL
* @param url 目标 URL / Target URL
* @return 响应内容 / Response content
*/
private String fetchUrl(String url) {
try {
// 构建 HTTP 请求
// Build HTTP request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url)) // 设置请求 URI / Set request URI
.GET() // 设置为 GET 请求 / Set as GET request
.build();
// 发送请求并获取响应
// Send request and get response
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body(); // 返回响应体 / Return response body
} catch (Exception e) {
throw new RuntimeException("Failed to fetch: FunTester - " + url, e);
}
}
}
注意关键细节:我们为 HttpClient 配置了一个虚拟线程执行器。这意味着每个 HTTP 请求都在虚拟线程上运行,允许大规模并发而不会耗尽资源。这就像给每个请求分配了一个轻量级的协程,而不是重量级的线程。
性能提升是巨大的。在传统的每个请求一个线程的模型中,你可能在达到数千个并发请求之前就会遇到内存或 CPU 限制。有了虚拟线程,这个限制实际上消失了。你可以轻松处理数万个甚至数十万个并发请求,而不用担心资源耗尽。
我们做了个基准测试,对 10,000 个并发 HTTP 请求进行测试,结果如下:
虚拟线程实现了响应式级别的性能,同时代码更易于阅读且是命令式的。这就是虚拟线程的魅力所在——用同步代码的简洁性,获得异步代码的性能。
JDK HttpClient 和虚拟线程的结合为 Java 开发者提供了一个强大的现代工具包,用于构建高并发的 HTTP 应用程序。你获得了同步、命令式代码的简洁性,以及通常为复杂的响应式或异步框架保留的可扩展性。
虚拟线程使高并发民主化——你不再需要成为响应式编程专家才能构建可扩展的系统。JDK HttpClient 提供了一个健壮的标准化 HTTP 客户端,许多情况下无需第三方依赖。这意味着你可以减少项目依赖,降低维护成本。