Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java 21 虚拟线程的陷阱:我们在 TPC-C for PostgreSQL 中遭遇死锁

Java 21 虚拟线程的陷阱:我们在 TPC-C for PostgreSQL 中遭遇死锁

作者头像
深度学习与Python
发布于 2024-01-24 10:58:10
发布于 2024-01-24 10:58:10
64100
代码可运行
举报
运行总次数:0
代码可运行

作者 | Evgeniy Ivanov

译者 | 平川

策划 | Tina

本文最初发布于 YDB.TECH。

Java 21 哲学家就餐问题

在上一篇关于 TPC-C 的文章中,我们讨论了 Benchbase 项目中 TPC-C 原始实现的一些缺点(尽管如此,它还是很棒)。其中一个缺点是因生成的物理线程太多所导致的并发性限制,我们通过切换到 Java 21 虚拟线程解决了这个问题。后来我们发现,像往常一样,天下没有免费的午餐。这篇文章中展示了一个案例研究,我们在 TPC-C for PostgreSQL 中遇到了虚拟线程死锁。

这篇文章对正在考虑切换到虚拟线程的 Java 开发人员可能会有所帮助。我们着重强调了虚拟线程潜在的一个重要问题:死锁可能是不可预测的,因为它们可能发生在你所使用的库的深处。幸运的是,调试很简单,我们将探讨如何在发生死锁时找到它们。

我们为什么要在 YDB 的博客上讨论 PostgreSQL

PostgreSQL 是一个开源数据库管理系统,以高性能、丰富的特性集、先进的 SQL 遵从性以及充满活力的支持性社区而闻名。如果不考虑水平可扩展性和容错性,那么它是很棒的。最终,你会选择基于 PostgreSQL 的第三方解决方案,比如 Citus,它实现了 PostgreSQL 分片。养一只大象可能很有趣,但管理一群大象是一项挑战,特别是如果你希望维护多个一致的副本,并使用序列化隔离执行分布式事务。

与此相反,YDB 一开始设计时就是一个分布式数据库管理系统。YDB 将分布式事务作为一等公民,默认即在序列化隔离级别上运行。现在,我们正在积极地兼容 PostgreSQL,因为我们看到,PostgreSQL 用户对现有应用程序的自动扩展和容错性有着强烈的需求。这就是我们维护 TPC-C for PostgreSQL 的原因(我们希望很快将其合并到上游 Benchbase 项目中)。

背景和动机简述

首先,我们回顾下一些基本概念:并发、并行执行以及异步与同步请求。

并发意味着任务在同一时间以并行或顺序的方式执行。例如,你可能有两个活动:在编辑器中编写代码和与同事在 Slack 上聊天。你可以同时执行这两项任务,但不是并行执行。或者你可以带着你的狗散步,同时和朋友打电话。同样,你可以同时执行这两项任务,但这一次是并行执行。

现在,考虑一下应用程序向数据库发出请求的情况。请求通过网络发送,数据库提供服务,将应答发送回应用程序。注意,网络往返可能是请求中成本最高的部分,可能需要几毫秒。在等待回复时,你可以在应用程序端做些什么呢?

  1. 请求可能是同步的,也就是说,它将阻塞调用线程。这种方法的代码很简单:第 1 行发起有请求;第 2 行处理响应:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
String userName = get_username_from_db(userId);
System.out.printf("Hello, %s!", userName);
  1. 请求可能是异步的。线程不会阻塞而是继续执行,而请求是并行处理的:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
CompletableFuture<String> userNameFuture = get_username_from_db(userId);

// 注意,这是一种回调,它不会在"这里"执行,
// 甚至在某些时候,它将与线程并行执行。
// 在现实场景中,你将不得不使用互斥。
userNameFuture.thenAccept(userName -> {
    System.out.println("Hello, %s!", userName);
});
execute_something_else();
userNameFuture.get(); // 等待请求完成

在每一种情况下都有两个并发任务:线程正在等待数据库的回复,而数据库正在处理请求。同步代码的编写非常简单,而且很容易阅读。但是,如果需要同时向数据库发出数千个请求,该怎么办呢?你必须为每个请求生成一个线程。在 Linux 中生成线程的成本很低,但生成的线程太多会令人非常担忧:

  1. 每个线程都需要一个堆栈。你分配的内存不能小于系统页面的大小,而页面的大小通常约为 4 KiB,除非你使用大页,其默认大小为 2 MiB。
  2. Linux 有一个调度器。如果有重置按钮的话,你可以尝试生成 10 万个准备执行的线程。

这就是在 Java 21 之前没有办法编写高并发性同步代码的原因:无法生成许多线程。Go 语言彻底改变了这一点:goroutine 提供了非常轻量级的并发,因此你可以高效地编写同步代码。建议你看下 Dmitry Vyukov 做的这个 关于 Go 调度器的演讲。Java 21 引入了虚拟线程,它在很多方面和 goroutine 类似。请记住,goroutine 和虚拟线程并不是一项新发明,而是用户级线程这一古老概念的转世。

现在就可以理解 Benchbase TPC-C 原始实现中数据库同步请求的问题了。要使数据库能够处理高负载,就必须运行许多 TPC-C 仓库,生成许多线程。在使用物理线程时,我们无法运行超过 3 万个终端线程,而在使用虚拟线程时,我们可以轻松拥有数十万个终端虚拟线程。

死锁很容易

假设你已经有了多线程 Java 代码。添加一个使用虚拟线程的选项非常简单,而且非常有益。只要简单地使用新的虚拟线程构建器替换标准线程创建代码,你的应用程序就可以处理数千个并发任务了,而且不会产生与物理线程相关的开销。下面这个例子来自我们的 TPC-C 实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (useRealThreads) {
    thread = new Thread(worker);
} else {
    thread = Thread.ofVirtual().unstarted(worker);
}

这样就行了。现在,你在使用虚拟线程了。在后台,Java 虚拟机会创建一个线程池carrier threads,它会执行virtual threads。这种转换看起来很完美,直到你的应用程序意外停止。

我们的 PostgreSQL TPC-C 实现利用了 c3p0 连接池。TPC-C 标准规定,每个终端都必须有自己的连接。然而,在许多实际的场景中,这是不现实的。因此,我们包含了一个选项用于限制数据库连接的数量。

终端的数量远远大于可用连接的数量。因此,部分终端必须等待会话变为可用,即由另一个终端释放。

当我们初次运行 TPC-C 时,应用程序意外停止了。幸运的是,调试很简单:

  1. 使用jstack -p <PID>捕获线程堆栈。
  2. 使用jcmd <PID> Thread.dump_to_file -format=text jcmd.dump.1创建更详细的当前状态转储,其中包括有关carrier threadsvirtual threads的信息。

经过研究,我们发现一些等待会话的虚拟线程锚定了它们的载体线程。下面是一个出现这种情况的虚拟线程的堆栈:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#7284 "TPCCWorker<7185>" virtual
      java.base/java.lang.Object.wait0(Native Method)
      java.base/java.lang.Object.wait(Object.java:366)
      com.mchange.v2.resourcepool.BasicResourcePool.awaitAvailable(BasicResourcePool.java:1503)
      com.mchange.v2.resourcepool.BasicResourcePool.prelimCheckoutResource(BasicResourcePool.java:644)
      com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:554)
      com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutAndMarkConnectionInUse(C3P0PooledConnectionPool.java:758)
      com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:685)
      com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:140)
      com.oltpbenchmark.api.BenchmarkModule.makeConnection(BenchmarkModule.java:108)
      com.oltpbenchmark.api.Worker.doWork(Worker.java:428)
      com.oltpbenchmark.api.Worker.run(Worker.java:304)
      java.base/java.lang.VirtualThread.run(VirtualThread.java:309)

下面是其载体线程的堆栈:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
"ForkJoinPool-1-worker-254" #50326 [32859] daemon prio=5 os_prio=0 cpu=12.39ms elapsed=489.99s tid=0x00007f3810003140  [0x00007f37abafe000]
   Carrying virtual thread #7284
        at jdk.internal.vm.Continuation.run(java.base@21.0.1/Continuation.java:251)
        at java.lang.VirtualThread.runContinuation(java.base@21.0.1/VirtualThread.java:221)
        at java.lang.VirtualThread$$Lambda/0x00007f3c2424e410.run(java.base@21.0.1/Unknown Source)
        at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(java.base@21.0.1/ForkJoinTask.java:1423)
        at java.util.concurrent.ForkJoinTask.doExec(java.base@21.0.1/ForkJoinTask.java:387)
        at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(java.base@21.0.1/ForkJoinPool.java:1312)
        at java.util.concurrent.ForkJoinPool.scan(java.base@21.0.1/ForkJoinPool.java:1843)
        at java.util.concurrent.ForkJoinPool.runWorker(java.base@21.0.1/ForkJoinPool.java:1808)
        at java.util.concurrent.ForkJoinWorkerThread.run(java.base@21.0.1/ForkJoinWorkerThread.java:188)

如你所见,线程在Object.wait()(一个与synchronized搭配使用的方法)中夯住了。这将导致载体线程被锚定,也就是说它不会被释放用于执行其他虚拟线程。同时,会话持有者在等待 I/O 操作时释放了它们的载体线程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
java.base/java.lang.VirtualThread.park(VirtualThread.java:582)
      java.base/java.lang.System$2.parkVirtualThread(System.java:2639)
      java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:54)
      java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:369)
      java.base/sun.nio.ch.Poller.pollIndirect(Poller.java:139)
      java.base/sun.nio.ch.Poller.poll(Poller.java:102)
      java.base/sun.nio.ch.Poller.poll(Poller.java:87)
      java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:175)
      java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:201)
      java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:309)
      java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:346)
      java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:796)
      java.base/java.net.Socket$SocketInputStream.read(Socket.java:1099)
      java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:489)
      java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:483)
      java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:70)
      java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1461)
      java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:1066)
      org.postgresql.core.VisibleBufferedInputStream.readMore(VisibleBufferedInputStream.java:161)
      org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:128)
      org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:113)
      org.postgresql.core.VisibleBufferedInputStream.read(VisibleBufferedInputStream.java:73)
      org.postgresql.core.PGStream.receiveChar(PGStream.java:465)
      org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2155)
      org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:574)
      org.postgresql.jdbc.PgStatement.internalExecuteBatch(PgStatement.java:896)
      org.postgresql.jdbc.PgStatement.executeBatch(PgStatement.java:919)
      org.postgresql.jdbc.PgPreparedStatement.executeBatch(PgPreparedStatement.java:1685)
      com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeBatch(NewProxyPreparedStatement.java:2544)
      com.oltpbenchmark.benchmarks.tpcc.procedures.NewOrder.newOrderTransaction(NewOrder.java:214)
      com.oltpbenchmark.benchmarks.tpcc.procedures.NewOrder.run(NewOrder.java:147)
      com.oltpbenchmark.benchmarks.tpcc.TPCCWorker.executeWork(TPCCWorker.java:66)
      com.oltpbenchmark.api.Worker.doWork(Worker.java:442)
      com.oltpbenchmark.api.Worker.run(Worker.java:304)
      java.base/java.lang.VirtualThread.run(VirtualThread.java:309)

这就导致了以下情况:

  1. 所有的载体线程都被会话等待者锚定,也就是说没有载体线程是可用的。
  2. 持有会话的虚拟线程无法完成任务并释放会话。

死锁很容易!

JEP 444 指出:

在两种情况下,虚拟线程在阻塞操作期间无法卸载,因为它被锚定在它的载体线程上: 当它执行同步块或方法中的代码时,或者当它执行本机方法或外部函数时。

问题是,这种同步代码可能深嵌在你所使用的库中。在我们的示例中,它位于 c3p0 库中。因此,修复很简单:我们只需用java.util.concurrent.Semaphore封装连接。通过这种修改,虚拟线程会被阻塞在信号量上,关键的是,载体线程得以释放,而不是在 c3p0 中陷入绝境。因此,我们永远不会阻塞在 c3p0 内部,因为我们只在有空闲会话可用时才进入 c3p0 代码。

小结

这是弗雷德·布鲁克斯所著《人月神话》一书的封面。这本书的封面艺术版权属于出版商 Addison-Wesley 或封面艺术家。

尽管软件开发已经发展了几十年,但似乎仍然没有什么银弹。不过,Java 21 虚拟线程是一个了不起的特性,如果使用得当,可以带来显著的好处:即使并发很高,也很容易编写出高效的异步代码。

原文链接:

https://blog.ydb.tech/how-we-switched-to-java-21-virtual-threads-and-got-deadlock-in-tpc-c-for-postgresql-cca2fe08d70b

声明:本文为 InfoQ 翻译,未经许可禁止转载。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-01-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 InfoQ 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java19 正式 GA!看虚拟线程如何大幅提高系统吞吐量
我们常用的 Java 线程与系统内核线程是一一对应的,系统内核的线程调度程序负责调度 Java 线程。为了增加应用程序的性能,我们会增加越来越多的 Java 线程,显然系统调度 Java 线程时,会占据不少资源去处理线程上下文切换。
PPPHUANG
2022/09/21
1K0
Java 13 明天发布,最新最全新特性解读
2017年8月,JCP执行委员会提出将Java的发布频率改为每六个月一次,新的发布周期严格遵循时间点,将在每年的3月份和9月份发布。
IT大咖说
2019/09/17
7130
Java 13 明天发布,最新最全新特性解读
【TPC-C】TPC-C标准化基准测试设计RDBMS的相关表结构
TPC 是事务处理性能委员会组织,该委员会致力于制定和维护一系列标准化的基准测试,以评估商业计算系统的性能。其中最著名的是一系列用于评估计算机系统性能的基准测试。
SarPro
2024/05/24
7490
【TPC-C】TPC-C标准化基准测试设计RDBMS的相关表结构
Jvm线程堆分析
同样,也有一些工具可以很方便的对线程的stack信息进行可视化的分析: fastthread就是一个在线分析线程stack的工具 fastthread使用界面:
冬天里的懒猫
2021/08/12
6770
Jvm线程堆分析
聊聊jvm的Stack Memory
序 本文主要研究一下jvm的Stack Memory Java-Heap-Stack-Memory.png Virtual Machine Stack 每个jvm线程都有一个私有的Virtual Machine Stack,它在线程同时被创建 该stack主要用于存储frames,即所谓的stack frames 每个方法在执行的时候都会创建一个stack frame,用于存储方法的局部变量、返回值、Operand stack等 Stack Memory Stack Memory是执行线程时所使用的内存
code4it
2019/04/01
1.6K0
聊聊jvm的Stack Memory
5 年了,你连 TPC-C/H 测试都不知道,ChatGPT 分分钟取代你
简介:本文部分 TPC 观点,使用 ChatGPT 生成。拉到最后,可扫码入群体验与 ChatGPT 机器人对话
Lenis
2023/03/02
1.7K1
5 年了,你连 TPC-C/H 测试都不知道,ChatGPT 分分钟取代你
Spring4.0+Mybatis整合时占位符无法读取jdbc.properties的问题
1、在使用Spring+Mybatis整合时遇到了一个问题,在bean.xml配置文件引用外部jdbc.properties的时候报错,如下所示:
别先生
2021/03/04
2.1K0
[翻译]如何分析Java线程dumps
这是关于故障诊断文章的第二篇,翻译自《How to Analyze Java Thread Dumps》,原文地址:https://dzone.com/articles/how-analyze-java-thread-dumps
LNAmp
2018/09/05
1K0
Java 13 新功能介绍
自从 Oracle 调整了 Java 的版本发布节奏之后,Java 版本发布越来越快,虽然都说 Java 版本任他发,我用 Java 8,不过新版本的 Java 功能还是要学习一下的。
未读代码
2021/08/12
4580
设置COMPOSE_PROJECT_NAME应避免使用下划线,容器间无法建立连接
设置docker compose的COMPOSE_PROJECT_NAME环境变量时,在有多个单词时,尽量不要使用下划线,因为COMPOSE_PROJECT_NAME会作为container_name的一部分。 而container_name在容器网络中会作为“域名”。而域名,是不接受非ldh ascii字符的,即不接受下划线。
xuing
2023/03/11
1.1K0
设置COMPOSE_PROJECT_NAME应避免使用下划线,容器间无法建立连接
性能加速包:SpringBoot 2.7&JDK 17,你敢尝一尝吗
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
京东技术
2024/01/27
1.8K0
性能加速包:SpringBoot 2.7&JDK 17,你敢尝一尝吗
虚拟线程原理及性能分析
JDK21 在 9 月 19 号正式发布,带来了较多亮点,其中虚拟线程备受瞩目,毫不夸张的说,它改变了高吞吐代码的编写方式,只需要小小的变动就可以让目前的 IO 密集型程序的吞吐量得到提升,写出高吞吐量的代码不再困难。
得物技术
2023/12/06
1.1K0
虚拟线程原理及性能分析
java.base.jmod
/Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home/jmods$ jmod list java.base.jmod|wc -l 5761
一个会写诗的程序员
2018/08/17
1.1K0
JFinal开发web项目出现故障小记
作为中国优秀的开源项目之中的一个JFinal有着极速开发的优点,是中小型应用开发的首选。在导师的建议下。我使用了JFinal来开发一个Java服务端应用,官方教程非常easy。就几十页(当然是中文的),学起来非常快。
全栈程序员站长
2022/07/08
1.1K0
使用benchmarksql测试数据库处理能力
我们所处行业的核心应用业务,当前还是传统的OLTP业务,应用系统使用 java 开发,并且不建议使用存储过程,使用 benchmarksql 压测数据库最公平,既可以测试数据库性能,也可以测试JDBC驱动。
雪人
2025/04/07
1040
CPU占用100%排查过程
https://blog.csdn.net/zxh87/article/details/52137335
spilledyear
2019/03/08
4.8K0
CPU占用100%排查过程
Java编程架构详解——Tomcat 中的 NIO 源码分析
文将介绍 Tomcat 中的 NIO 使用,使大家对 Java NIO 的生产使用有更加直观的认识。
慕容千语
2019/06/11
1.1K0
Docker 最佳实战:Docker 部署单节点 ElasticSearch 实战
今天分享的内容是 Docker 最佳实战「2024」 系列文档中的 Docker 部署单节点 ElasticSearch 实战。
运维有术
2024/05/08
1.8K0
Docker 最佳实战:Docker 部署单节点 ElasticSearch 实战
Kubernetes 部署 Compute storage coupled 模式 Doris 高可用集群实战指南
今天分享的主题是:如何在 k8s 集群上部署 Compute storage coupled(存算耦合) 模式的 Doris 高可用集群?
运维有术
2024/11/29
1990
Kubernetes 部署 Compute storage coupled 模式 Doris 高可用集群实战指南
Kubernetes 部署 Compute storage coupled 模式 Doris 高可用集群实战指南
今天分享的主题是:如何在 k8s 集群上部署 Compute storage coupled(存算耦合) 模式的 Doris 高可用集群?
运维有术
2024/12/02
1680
Kubernetes 部署 Compute storage coupled 模式 Doris 高可用集群实战指南
推荐阅读
相关推荐
Java19 正式 GA!看虚拟线程如何大幅提高系统吞吐量
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验