
本文深入探讨了java服务器应用中处理高并发和数据库交互的多种并发模型,包括传统的阻塞i/o、基于回调的非阻塞编程以及java 21引入的虚拟线程。文章分析了每种模型的优缺点、适用场景及其对jdbc等同步api的影响,并强调了虚拟线程作为未来高并发应用开发首选解决方案的颠覆性作用。
在构建高并发的Java服务器应用时,如何高效处理I/O密集型操作(尤其是数据库访问)是核心挑战。针对每秒千次请求(QPS)的场景,并且每个请求都包含数据库读写等顺序操作,开发者通常面临多种并发模型选择。本教程将详细解析这些模型,并提供实践指导。
传统的Java服务器应用通常采用“每个请求一个线程”的模型。在这种模式下,当一个请求到达时,服务器会从线程池中分配一个平台线程(即操作系统线程)来处理该请求。如果请求涉及I/O操作(如通过JDBC访问数据库),该线程会阻塞,直到I/O操作完成。
工作原理: 假设我们有一个包含200个线程的线程池。当1000 QPS的请求到来时,每个请求被分配一个独立的线程。该线程负责执行所有业务逻辑,包括等待数据库响应。
优点:
缺点:
立即学习“Java免费学习笔记(深入)”;
非阻塞I/O(NIO)和反应式编程旨在解决传统阻塞模型中线程资源消耗大的问题。其核心思想是,当一个I/O操作发生时,线程不等待结果,而是注册一个回调函数,然后立即返回去处理其他请求。当I/O操作完成时,系统会通过回调机制通知相应的处理逻辑。
工作原理: 使用一个或少数几个线程(事件循环线程)来处理大量的并发连接。当一个请求需要进行数据库查询时,它会发起一个非阻塞的数据库请求(如果存在),然后将后续处理逻辑封装成回调函数,当前线程则立即释放去处理其他请求。
优点:
缺点:
立即学习“Java免费学习笔记(深入)”;
在Java生态中,JDBC(Java Database Connectivity)是标准的数据库访问API。然而,JDBC从设计之初就是同步阻塞的。这意味着,无论你的应用层代码如何实现非阻塞逻辑,一旦调用JDBC方法,当前线程就必须等待数据库响应,从而导致阻塞。
问题: 如果整个应用的其他部分都设计为非阻塞,但数据库访问仍然使用JDBC,那么所有非阻塞的优势都将因为JDBC的阻塞特性而丧失。在这种情况下,非阻塞模型的复杂性反而成为额外的负担,而无法带来性能收益。
解决方案:
R2DBC (Reactive Relational Database Connectivity): 这是一个为关系型数据库提供反应式编程接口的规范。它允许开发者以非阻塞的方式与数据库交互,与Spring WebFlux等反应式框架无缝集成。许多数据库(如MySQL)都有对应的R2DBC驱动。
// 示例:R2DBC连接和查询 (概念性代码)
Mono<ConnectionFactory> connectionFactory = ConnectionFactories.get("r2dbc:mysql://localhost:3306/testdb");
connectionFactory.flatMap(factory ->
Mono.from(factory.create())
.flatMap(connection ->
Mono.from(connection.createStatement("SELECT id, name FROM users")
.execute())
.flatMapMany(result ->
result.map((row, rowMetadata) ->
new User(row.get("id", Integer.class), row.get("name", String.class))
)
)
.doFinally(signalType -> connection.close()) // 确保关闭连接
)
).subscribe(user -> System.out.println("User: " + user.getName()));Oracle ADBA (Asynchronous Database Access API): 曾是Oracle尝试为JDBC提供异步替代的方案,但最终被废弃。其主要原因是Java平台正在走向一个更优雅的解决方案——虚拟线程。
Java 21(以及Project Loom在之前的版本中作为预览功能)引入的虚拟线程(Virtual Threads)彻底改变了Java并发编程的格局。虚拟线程是一种轻量级的、由JVM管理的线程,它们映射到少量的平台线程上。
工作原理: 当一个虚拟线程执行阻塞I/O操作(如JDBC调用)时,JVM会“卸载”该虚拟线程,使其不再占用底层的平台线程。一旦I/O操作完成,JVM会“重新挂载”该虚拟线程,并将其调度到一个可用的平台线程上继续执行。对于开发者而言,编写的代码仍然是同步阻塞风格的,但底层运行时却实现了高效的非阻塞I/O。
优点:
示例: 使用虚拟线程,你仍然可以像往常一样编写JDBC代码,但将其提交给一个使用虚拟线程的执行器:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.concurrent.Executors;
public class VirtualThreadJdbcExample {
public static void main(String[] args) {
// 使用虚拟线程的ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
try {
// 传统的JDBC阻塞调用,但在虚拟线程中执行不会阻塞底层平台线程
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "user", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, name FROM users WHERE id = " + taskId);
if (rs.next()) {
System.out.println("Task " + taskId + ": User ID: " + rs.getInt("id") + ", Name: " + rs.getString("name"));
}
rs.close();
stmt.close();
conn.close();
} catch (Exception e) {
System.err.println("Task " + taskId + " error: " + e.getMessage());
}
});
}
} // executor.close() 会等待所有任务完成
System.out.println("All JDBC tasks submitted.");
}
}在上述代码中,尽管DriverManager.getConnection()和stmt.executeQuery()是阻塞调用,但由于它们在虚拟线程中执行,当这些调用阻塞时,JVM会自动将虚拟线程“卸载”,释放底层的平台线程去执行其他虚拟线程,从而实现了高效率和高并发。
对于Java服务器应用中的高并发和数据库访问场景,选择合适的并发模型至关重要。
Java 21及更高版本:优先使用虚拟线程。 虚拟线程是目前处理I/O密集型高并发任务的最佳解决方案。它结合了传统阻塞编程的易用性和非阻塞编程的高伸缩性,并且能与现有JDBC等同步API无缝集成。在大多数情况下,虚拟线程将使“阻塞 vs. 非阻塞”的选择变得无关紧要,因为你可以用阻塞的编程风格实现非阻塞的性能。
Java 21以下版本:
性能测试: 无论选择哪种模型,都必须进行充分的负载测试和性能基准测试。实际性能往往受多种因素影响,包括硬件、操作系统、JVM配置、数据库性能以及具体的业务逻辑。
总之,Java 21的虚拟线程为Java并发编程带来了革命性的进步,使得开发者能够以更简单、更高效的方式构建高伸缩性的服务器应用,尤其是在涉及传统阻塞I/O(如JDBC)的场景中。在现代Java开发中,应优先考虑拥抱虚拟线程。
以上就是Java服务器并发模型:从阻塞到非阻塞,再到虚拟线程的演进与实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号