java使用虚拟线程遇到的问题

虚拟线程

虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程,由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。它们可以更有效地运行以thread-per-request(每个请求一个线程,就大大降低了线程上下文的切换)的方式编写的服务器应用程序,从而实现更高的吞吐量和更少的硬件浪费。

虚拟线程有几个核心的对象:

  • Continuation:译为“续延”,是用户真实任务的包装器,虚拟线程会把任务包装到一个Continuation实例中,当任务需要阻塞挂起的时候,会调用Continuation的yield操作进行阻塞
  • Scheduler:译为“调度器”,会把任务提交到一个平台线程池中执行,虚拟线程中维护了一个默认的调度器DEFAULT_SCHEDULER,这是一个 ForkJoinPool 实例,最大线程数默认是系统核心线程数,最大为 256,可以通过 jdk.virtualThreadScheduler.maxPoolSize 进行设置。
  • carrier:载体线程(Thread对象),指的是负责执行虚拟线程中任务的平台线程。
  • runContinuation:一个Runnable对象,用于在任务运行或继续之前,虚拟线程将装载到当前线程上。当任务完成或完成时,将其卸载

如何创建虚拟线程

1、使用**Thread.startVirtualThread**

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class VirtualThreadTest {
  public static void main(String[] args) {
    CustomThread customThread = new CustomThread();
    Thread.startVirtualThread(customThread);
  }
}
static class CustomThread implements Runnable {
  @Override
  public void run() {
    System.out.println("CustomThread run");
  }
}

2、使用**Thread.ofVirtual**

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class VirtualThreadTest {
  public static void main(String[] args) {
    CustomThread customThread = new CustomThread();
    // 创建不启动
    Thread unStarted = Thread.ofVirtual().unstarted(customThread);
    unStarted.start();
    // 创建直接启动
    Thread.ofVirtual().start(customThread);
  }
}
static class CustomThread implements Runnable {
  @Override
  public void run() {
    System.out.println("CustomThread run");
  }
}

3、使用 ThreadFactory 创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class VirtualThreadTest {
  public static void main(String[] args) {
    CustomThread customThread = new CustomThread();
    ThreadFactory factory = Thread.ofVirtual().factory();
    Thread thread = factory.newThread(customThread);
    thread.start();
  }
}
static class CustomThread implements Runnable {
  @Override
  public void run() {
    System.out.println("CustomThread run");
  }
}

4、使用Executors.newVirtualThreadPerTaskExecutor创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class VirtualThreadTest {
  public static void main(String[] args) {
    CustomThread customThread = new CustomThread();
    ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    executor.submit(customThread);
  }
}
static class CustomThread implements Runnable {
  @Override
  public void run() {
    System.out.println("CustomThread run");
  }
}

springboot3.2与虚拟线程

随着springboot3.2发布,在使用jdk21的情况下。可以使用如下配置开启虚拟线程

1
spring.threads.virtual.enabled=true

我们写了个简单的demo,进行性能测试。结果如预期所料,响应时间和吞吐率都是虚拟线程完胜。

但改为使用数据库以后,发现性能相比于不使用虚拟线程反而下降了。并且随着请求数量增加,发现数据库连接被占满。Connection is not available, request timed out after 906ms

这是为什么呢?

虚拟线程与synchronized

通过阅读官方虚拟线程文档,目前虚拟线程与synchronized关键字的适配尚未完成。如果遇到synchronized,性能反而会下降。

There are two scenarios in which a virtual thread cannot be unmounted during blocking operations because it is pinned to its carrier:

  1. When it executes code inside a synchronized block or method, or
  2. When it executes a native method or a foreign function.

显然,我们的代码中并无此类场景。所以问题应该是在jdbc或者mysql驱动。起初我们以为是hikari的问题,但在报错的时候,我们看到了如下异常。基本上可以确定是mysql驱动的问题

需要将使用synchronized的代码,全部改为使用**ReentrantLock**来实现。同时我们也看到了mysql和mybatis社区已经有人反馈此问题,至此我们决定暂时不启用虚拟线程。

**UPDATE2024-05-08:**由于我们在持续跟踪此问题,随着mysql-connector-j 9.0版本和mybatis3.5.16版本发布,代码均已按预期修改。在我们未对代码做额外优化的情况下,各方面性能得到了提升!

我们在4c8g的机器上,使用虚拟线程的配置如下。可以达到最高10k并发、40k tps

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
spring.threads.virtual.enabled=true
#set tomcat thread pool
server.tomcat.threads.max=3000
server.tomcat.threads.min-spare=3000
server.tomcat.max-connections=10000
server.tomcat.accept-count=1000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.maximum-pool-size=3000
spring.datasource.hikari.minimum-idle=2000
spring.datasource.hikari.connection-timeout=3000
spring.datasource.hikari.max-lifetime=3600000
spring.datasource.hikari.idle-timeout=1200000

总结

1、强制使用**ReentrantLock**:虚拟线程可以更好的利用cpu,支持了java版本的协程。但在当前版本无法和*synchronized*关键字很好的适配。在项目中如果需要线程同步的场景,

2、统一使用虚拟线程:在大多数场景可以抛弃掉原有的各种线程池,由jvm为我们做调度。再也不怕因线程太多而导致oom了

3、在会调用到native方法和jni的地方:不应使用虚拟线程

4、虚拟线程适用于长时间的IO密集型任务,而不适用于长时间的CPU密集型任务

5、虚拟线程开启后,应使用较新版本的mybatis和mysql驱动

6、经过测试,虚拟线程和webfulx时吞吐量大致相当。但使用虚拟线程无需额外的学习成本。虚拟线程吞吐量远超普通线程池!

References

虚拟线程与synchronized

虚拟线程

SpringBoot3.2

mybatis-3.5.16

Replace synchronized with ReentrantLock

hikari

淘宝对虚拟线程的研究

Talk is cheap, show me the bug/code.
使用 Hugo 构建
主题 StackJimmy 设计