Java19 正式 GA!看虚拟线程如何大幅提高系统吞吐量

Java19新特性-虚拟线程(第一次预览) 众所周知,JVM 是一个多线程环境,它通过为我们提供了对操作系统线程的抽象,但是 Java 中的线程都只是对操作系统线程的一种简单封装,我们可以称之为“平台线程”。代价昂贵:创建平台线程的成本很高。每当创建一个平台线程时,操作系统都必须在堆栈中分配大量内存来存储线程的上下文、原生调用堆栈和 Java 调用堆栈。由于堆栈大小是固定的,这就导致了高昂的内存开销。上下文切换成本高:在多线程环境下,需要在不同线程间切换,这种上下文切换会消耗时间和资源。线程数量有限。 阅读详情

今天Java19 正式发布,带来了一个 Java 开发者垂涎已久的新特性—— 虚拟线程。在 Java 有这个新特性之前,Go语言的协程风靡已久,在并发编程领域可以说是叱咤风云。随着国内 Go 语言的快速发展与推广,协程好像成为了一个世界上最好语言的必备特性之一。Java19 虚拟线程就是来弥补这个空白的。本文将通过对虚拟线程的介绍,以及与 Go 协程的对比来带大家尝鲜 Java19 虚拟线程。

本文要点:

  • Java 线程模型
  • 平台线程与虚拟线程性能对比
  • Java 虚拟线程与 Go 协程对比
  • 如何使用虚拟线程

Java 线程模型

java 线程 与 虚拟线程

我们常用的 Java 线程与系统内核线程是一一对应的,系统内核的线程调度程序负责调度 Java 线程。为了增加应用程序的性能,我们会增加越来越多的 Java 线程,显然系统调度 Java 线程时,会占据不少资源去处理线程上下文切换。

近几十年来,我们一直依赖上述多线程模型来解决 Java 并发编程的问题。为了增加系统的吞吐量,我们要不断增加线程的数量,但机器的线程是昂贵的、可用线程数量也是有限的。即使我们使用了各种线程池来最大化线程的性价比,但是线程往往会在 CPU、网络或者内存资源耗尽之前成为我们应用程序的性能提升瓶颈,不能最大限度的释放硬件应该具有的性能。

为了解决这个问题 Java19 引入了虚拟线程(Virtual Thread)。在 Java19 中,之前我们常用的线程叫做平台线程(platform thread),与系统内核线程仍然是一一对应的。其中大量(M)的虚拟线程在较小数量(N)的平台线程(与操作系统线程一一对应)上运行(M:N调度)。多个虚拟线程会被 JVM 调度到某一个平台线程上执行,一个平台线程同时只会执行一个虚拟线程。

创建 Java 虚拟线程

新增线程相关 API

Thread.ofVirtual()和Thread.ofPlatform()是创建虚拟和平台线程的新API:

//输出线程ID 包括虚拟线程和系统线程 Thread.getId() 从jdk19废弃
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
//创建虚拟线程
Thread thread = Thread.ofVirtual().name("testVT").unstarted(runnable);
testVT.start();
//创建虚平台线程
Thread testPT = Thread.ofPlatform().name("testPT").unstarted(runnable);
testPT.start();

使用Thread.startVirtualThread(Runnable)快速创建虚拟线程并启动:

//输出线程ID 包括虚拟线程和系统线程
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
Thread thread = Thread.startVirtualThread(runnable);

 Thread.isVirtual()判断线程是否为虚拟线程:

//输出线程ID 包括虚拟线程和系统线程
Runnable runnable = () -> System.out.println(Thread.currentThread().isVirtual());
Thread thread = Thread.startVirtualThread(runnable);

Thread.join和Thread.sleep等待虚拟线程结束、使虚拟线程 sleep:

Runnable runnable = () -> System.out.println(Thread.sleep(10));
Thread thread = Thread.startVirtualThread(runnable);
//等待虚拟线程结束
thread.join();

Executors.newVirtualThreadPerTaskExecutor()创建一个 ExecutorService,该 ExecutorService 为每个任务创建一个新的虚拟线程:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
  executor.submit(() -> System.out.println("hello"));
}

支持与使用线程池和 ExecutorService 的现有代码互相替换、迁移。

注意:

因为虚拟线程在 Java19 中是预览特性,所以本文出现的代码需按以下方式运行:

  • 使用javac --release 19 --enable-preview Main.java编译程序,并使用java --enable-preview Main运行;
  • 或者使用java --source 19 --enable-preview Main.java运行程序;

是骡子是马

既然是为了解决平台线程的问题,那我们就直接测试平台线程与虚拟线程的性能。

测试内容很简单,并行执行一万个 sleep 一秒的任务,对比总的执行时间和所用系统线程数量。

为了监控测试所用系统线程的数量,编写如下代码:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
  ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
  ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
  System.out.println(threadInfo.length + " os thread");
}, 1, 1, TimeUnit.SECONDS);

调度线程池每一秒钟获取并打印系统线程数量,便于观察线程的数量。

public static void main(String[] args) {
  //记录系统线程数
  ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
  scheduledExecutorService.scheduleAtFixedRate(() -> {
    ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
    ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
    System.out.println(threadInfo.length + " os thread");
  }, 1, 1, TimeUnit.SECONDS);

  long l = System.currentTimeMillis();
  try(var executor = Executors.newCachedThreadPool()) {
    IntStream.range(0, 10000).forEach(i -> {
      executor.submit(() -> {
        Thread.sleep(Duration.ofSeconds(1));
        System.out.println(i);
        return i;
      });
    });
  }
  System.out.printf("耗时:%d ms", System.currentTimeMillis() - l);
}

首先我们使用Executors.newCachedThreadPool()来执行10000个任务,因为 newCachedThreadPool 的最大线程数量是Integer.MAX_VALUE,所以理论上至少会创建大几千个系统线程来执行。

输出如下(多余输出已省略):

//output
1
7142
3914 os thread
  
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
        at java.base/java.lang.Thread.start0(Native Method)
        at java.base/java.lang.Thread.start(Thread.java:1560)
        at java.base/java.lang.System$2.start(System.java:2526)

从上述输出可以看到,最高创建了 3914 个系统线程,然后继续创建线程时异常,程序终止。我们想通过大量系统线程提高系统的性能是不现实的,因为线程昂贵,资源有限。

现在我们使用固定大小为 200 的线程池来解决不能申请太多系统线程的问题:

public static void main(String[] args) {
  //记录系统线程数
  ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
  scheduledExecutorService.scheduleAtFixedRate(() -> {
    ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
    ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
    System.out.println(threadInfo.length + " os thread");
  }, 1, 1, TimeUnit.SECONDS);

  long l = System.currentTimeMillis();
  try(var executor = Executors.newFixedThreadPool(200)) {
    IntStream.range(0, 10000).forEach(i -> {
      executor.submit(() -> {
        Thread.sleep(Duration.ofSeconds(1));
        System.out.println(i);
        return i;
      });
    });
  }

  System.out.printf("耗时:%dms\n", System.currentTimeMillis() - l);
}

输出如下:

//output
1
9987
9998
207 os thread
耗时:50436ms

使用固定大小线程池后没有了创建大量系统线程导致失败的问题,能正常跑完任务,最高创建了 207 个系统线程,共耗时 50436ms。

再来看看使用虚拟线程的结果:

public static void main(String[] args) {
  ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
  scheduledExecutorService.scheduleAtFixedRate(() -> {
    ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
    ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
    System.out.println(threadInfo.length + " os thread");
  }, 10, 10, TimeUnit.MILLISECONDS);

  long l = System.currentTimeMillis();
  try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10000).forEach(i -> {
      executor.submit(() -> {
        Thread.sleep(Duration.ofSeconds(1));
        System.out.println(i);
        return i;
      });
    });
  }

  System.out.printf("耗时:%dms\n", System.currentTimeMillis() - l);
}

使用虚拟线程的代码和使用固定大小的只有一词只差,将Executors.newFixedThreadPool(200)替换为Executors.newVirtualThreadPerTaskExecutor()。

输出结果如下:

//output
1
9890
15 os thread
耗时:1582ms

由输出可见,执行总耗时 1582 ms,最高使用系统线程 15 个。结论很明显,使用虚拟线程比平台线程要快很多,并且使用的系统线程资源要更少。

如果我们把刚刚这个测试程序中的任务换成执行了一秒钟的计算(例如,对一个巨大的数组进行排序),而不仅仅是 sleep 1秒钟,即使我们把虚拟线程或者平台线程的数量增加到远远大于处理器内核数量都不会有明显的性能提升。因为虚拟线程不是更快的线程,它们运行代码的速度与平台线程相比并无优势。虚拟线程的存在是为了提供更高的吞吐量,而不是速度(更低的延迟)。

如果你的应用程序符合下面两点特征,使用虚拟线程可以显著提高程序吞吐量:

  • 程序并发任务数量很高。
  • IO密集型、工作负载不受 CPU 约束。

虚拟线程有助于提高服务端应用程序的吞吐量,因为此类应用程序有大量并发,而且这些任务通常会有大量的 IO 等待。

Java vs Go

使用方式对比

Go 协程对比 Java 虚拟线程

定义一个 say() 方法,方法体是循环 sleep 100ms,然后输出index,将这个方法使用协程执行。

Go 实现:

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

Java 实现:

public final class VirtualThreads {
    static void say(String s) {
        try {
            for (int i = 0; i < 5; i++) {
                Thread.sleep(Duration.ofMillis(100));
                System.out.println(s);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        var worldThread = Thread.startVirtualThread(
            () -> say("world")
        );
        
        say("hello");
        
        // 等待虚拟线程结束
        worldThread.join();
    }
}

可以看到两种语言协程的写法很相似,总体来说 Java 虚拟线程的写法稍微麻烦一点,Go 使用一个关键字就能方便的创建协程。

Go 管道对比 Java 阻塞队列

在 Go 语言编程中,协程与管道的配合相得益彰,使用协程计算数组元素的和(分治思想):

Go 实现:

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}
    
func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
}

Java 实现:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;

public class main4 {
    static void sum(int[] s, int start, int end, BlockingQueue<Integer> queue) throws InterruptedException {
        int sum = 0;
        for (int i = start; i < end; i++) {
            sum += s[i];
        }
        queue.put(sum);
    }


    public static void main(String[] args) throws InterruptedException {
        int[] s = {7, 2, 8, -9, 4, 0};
        var queue = new ArrayBlockingQueue<Integer>(1);
        Thread.startVirtualThread(() -> {
            sum(s, 0, s.length / 2, queue);
        });
        Thread.startVirtualThread(() -> {
            sum(s, s.length / 2, s.length, queue);
        });
        int x = queue.take();
        int y = queue.take();

        System.out.printf("%d %d %d\n", x, y, x + y);
    }
}

因为 Java 中没有数组切片,所以使用数组和下标来代替。Java 中没有管道,用与管道相似的 BlockingQueue 来代替,可以实现功能。

协程实现原理对比

GO G-M-P 模型

Go 语言采用两级线程模型,协程与系统内核线程是 M:N 的,这一点与 Java 虚拟线程一致。最终 goroutine 还是会交给 OS 线程执行,但是需要一个中介,提供上下文。这就是 G-M-P 模型。

  • G: goroutine, 类似进程控制块,保存栈,状态,id,函数等信息。G 只有绑定到 P 才可以被调度。

  • M: machine, 系统线程,绑定有效的 P 之后,进行调度。

  • P: 逻辑处理器,保存各种队列 G。对于 G 而言,P 就是 cpu 核心。对于 M 而言,P 就是上下文。

  • sched: 调度程序,保存 GRQ(全局运行队列),M 空闲队列,P 空闲队列以及 lock 等信息。

队列

Go 调度器有两个不同的运行队列:

  • GRQ,全局运行队列,尚未分配给 P 的 G(在 Go1.1 之前只有 GRO 全局运行队列,但是因为全局队列加锁的性能问题加上了LRQ,以减少锁等待)。
  • LRQ,本地运行队列,每个 P 都有一个 LRQ,用于管理分配给P执行的 G。当 LRQ 中没有待执行的 G 时会从 GRQ 中获取。

hand off 机制

当 G 执行阻塞操作时,G-M-P 为了防止阻塞 M,影响 LRQ 中其他 G 的执行,会调度空闲 M 来执行阻塞 M LRQ 中的其他 G:

  1. G1 在 M1 上运行,P 的 LRQ 有其他 3 个 G;
  2. G1 进行同步调用,阻塞 M;
  3. 调度器将 M1 与 P 分离,此时 M1 下只运行 G1,没有 P。
  4. 将 P 与空闲 M2 绑定,M2 从 LRQ 选择其他 G 运行。
  5. G1 结束堵塞操作,移回 LRQ。M1 会被放置到空闲队列中备用。

work stealing机制

G-M-P 为了最大限度释放硬件性能,当 M 空闲时会使用任务窃取机制执行其他等待执行的 G:

  1. 有两个 P,P1,P2。
  2. 如果 P1 的 G 都执行完了,LRQ 为空,P1 就开始任务窃取。
  3. 第一种情况,P1从 GRQ 获取 G。
  4. 第二种情况,P1 从 GRQ 没有获取到 G,则 P1 从 P2 LRQ 中窃取G。

hand off 机制是防止 M 阻塞,任务窃取是防止 M 空闲。

Java 虚拟线程调度

基于操作系统线程实现的平台线程,JDK 依赖于操作系统中的线程调度程序来进行调度。而对于虚拟线程,JDK 有自己的调度器。JDK 的调度器没有直接将虚拟线程分配给系统线程,而是将虚拟线程分配给平台线程(这是前面提到的虚拟线程的 M:N 调度)。平台线程由操作系统的线程调度系统调度。

JDK 的虚拟线程调度器是一个在 FIFO 模式下运行的类似ForkJoinPool的线程池。调度器的并行数量取决于调度器虚拟线程的平台线程数量。默认情况下是 CPU 可用核心数量,但可以使用系统属性jdk.virtualThreadScheduler.parallelism进行调整。注意,这里的ForkJoinPool与ForkJoinPool.commonPool()不同,ForkJoinPool.commonPool()用于实现并行流,并在 LIFO 模式下运行。

ForkJoinPool和ExecutorService的工作方式不同,ExecutorService有一个等待队列来存储它的任务,其中的线程将接收并处理这些任务。而ForkJoinPool的每一个线程都有一个等待队列,当一个由线程运行的任务生成另一个任务时,该任务被添加到该线程的等待队列中,当我们运行Parallel Stream,一个大任务划分成两个小任务时就会发生这种情况。

为了防止线程饥饿问题,当一个线程的等待队列中没有更多的任务时,ForkJoinPool还实现了另一种模式,称为任务窃取, 也就是说:饥饿线程可以从另一个线程的等待队列中窃取一些任务。这和 Go G-M-P 模型中 work stealing 机制有异曲同工之妙。

虚拟线程的执行

通常,当虚拟线程执行 I/O 或 JDK 中的其他阻止操作(如BlockingQueue.take()时,虚拟线程会从平台线程上卸载。当阻塞操作准备完成时(例如,网络 IO 已收到字节数据),调度程序将虚拟线程挂载到平台线程上以恢复执行。

JDK 中的绝大多数阻塞操作会将虚拟线程从平台线程上卸载,使平台线程能够执行其他工作任务。但是,JDK 中的少数阻塞操作不会卸载虚拟线程,因此会阻塞平台线程。因为操作系统级别(例如许多文件系统操作)或 JDK 级别(例如Object.wait())的限制。这些阻塞操作阻塞平台线程时,将通过暂时增加平台线程的数量来补偿其他平台线程阻塞的损失。因此,调度器的ForkJoinPool中的平台线程数量可能会暂时超过 CPU 可用核心数量。调度器可用的平台线程的最大数量可以使用系统属性jdk.virtualThreadScheduler.maxPoolSize进行调整。这个阻塞补偿机制与 Go G-M-P 模型中 hand off 机制有异曲同工之妙。

在以下两种情况下,虚拟线程会被固定到运行它的平台线程,在阻塞操作期间无法卸载虚拟线程:

  1. 当在synchronized块或方法中执行代码时。
  2. 当执行native方法或foreign function时。

虚拟线程被固定不会影响程序运行的正确性,但它可能会影响系统的并发度和吞吐量。如果虚拟线程在被固定时执行 I/O或BlockingQueue.take() 等阻塞操作,则负责运行它的平台线程在操作期间会被阻塞。(如果虚拟线程没有被固定,那会执行 I/O 等阻塞操作时会从平台线程上卸载)

如何卸载虚拟线程

我们通过 Stream 创建 5 个未启动的虚拟线程,这些线程的任务是:打印当前线程,然后休眠 10 毫秒,然后再次打印线程。然后启动这些虚拟线程,并调用jion()以确保控制台可以看到所有内容:

public static void main(String[] args) throws Exception {
  var threads = IntStream.range(0, 5).mapToObj(index -> Thread.ofVirtual().unstarted(() -> {
    System.out.println(Thread.currentThread());
    try {
      Thread.sleep(10);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    System.out.println(Thread.currentThread());
  })).toList();

  threads.forEach(Thread::start);
  for (Thread thread : threads) {
    thread.join();
  }
}
//output
src [main] ~/Downloads/jdk-19.jdk/Contents/Home/bin/java --enable-preview main7                   
VirtualThread[#23]/runnable@ForkJoinPool-1-worker-3
VirtualThread[#22]/runnable@ForkJoinPool-1-worker-2
VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
VirtualThread[#25]/runnable@ForkJoinPool-1-worker-5
VirtualThread[#24]/runnable@ForkJoinPool-1-worker-4
VirtualThread[#25]/runnable@ForkJoinPool-1-worker-3
VirtualThread[#24]/runnable@ForkJoinPool-1-worker-2
VirtualThread[#21]/runnable@ForkJoinPool-1-worker-4
VirtualThread[#22]/runnable@ForkJoinPool-1-worker-2
VirtualThread[#23]/runnable@ForkJoinPool-1-worker-3

由控制台输出,我们可以发现,VirtualThread[#21] 首先运行在 ForkJoinPool 的线程 1 上,当它从 sleep 中返回时,继续在线程 4 上运行。

sleep 之后为什么虚拟线程从一个平台线程跳转到另一个平台线程?

我们阅读一下 sleep 方法的源码,会发现在 Java19 中 sleep 方法被重写了,重写后的方法里还增加了虚拟线程相关的判断:

public static void sleep(long millis) throws InterruptedException {
  if (millis < 0) {
    throw new IllegalArgumentException("timeout value is negative");
  }

  if (currentThread() instanceof VirtualThread vthread) {
    long nanos = MILLISECONDS.toNanos(millis);
    vthread.sleepNanos(nanos);
    return;
  }

  if (ThreadSleepEvent.isTurnedOn()) {
    ThreadSleepEvent event = new ThreadSleepEvent();
    try {
      event.time = MILLISECONDS.toNanos(millis);
      event.begin();
      sleep0(millis);
    } finally {
      event.commit();
    }
  } else {
    sleep0(millis);
  }
}

深追代码发现,虚拟线程 sleep 时真正调用的方法是 Continuation.yield:

@ChangesCurrentThread
private boolean yieldContinuation() {
  boolean notifyJvmti = notifyJvmtiEvents;
  // unmount
  if (notifyJvmti) notifyJvmtiUnmountBegin(false);
  unmount();
  try {
    return Continuation.yield(VTHREAD_SCOPE);
  } finally {
    // re-mount
    mount();
    if (notifyJvmti) notifyJvmtiMountEnd(false);
  }
}

也就是说 Continuation.yield 会将当前虚拟线程的堆栈由平台线程的堆栈转移到 Java 堆内存,然后将其他就绪虚拟线程的堆栈由 Java 堆中拷贝到当前平台线程的堆栈中继续执行。执行 IO 或BlockingQueue.take() 等阻塞操作时会跟 sleep 一样导致虚拟线程切换。虚拟线程的切换也是一个相对耗时的操作,但是与平台线程的上下文切换相比,还是轻量很多的。

其他

虚拟线程与异步编程

响应式编程解决了平台线程需要阻塞等待其他系统响应的问题。使用异步 API 不会阻塞等待响应,而是通过回调通知结果。当响应到达时,JVM 将从线程池中分配另一个线程来处理响应。这样,处理单个异步请求会涉及多个线程。

在异步编程中,我们可以降低系统的响应延迟,但由于硬件限制,平台线程的数量仍然有限,因此我们的系统吞吐量仍有瓶颈。另一个问题是,异步程序在不同的线程中执行,很难调试或分析它们。

虚拟线程通过较小的语法调整来提高代码质量(降低编码、调试、分析代码的难度),同时具有响应式编程的优点,能大幅提高系统吞吐量。

不要池化虚拟线程

因为虚拟线程非常轻量,每个虚拟线程都打算在其生命周期内只运行单个任务,所以没有池化虚拟线程的必要。

虚拟线程下的 ThreadLocal

public class main {
    private static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

    public static void getThreadLocal(String val) {
        stringThreadLocal.set(val);
        System.out.println(stringThreadLocal.get());
    }

    public static void main(String[] args) throws InterruptedException {

        Thread testVT1 = Thread.ofVirtual().name("testVT1").unstarted(() ->main5.getThreadLocal("testVT1 local var"));

        Thread testVT2 = Thread.ofVirtual().name("testVT2").unstarted(() ->main5.getThreadLocal("testVT2 local var"));

        testVT1.start();
        testVT2.start();

        System.out.println(stringThreadLocal.get());
        stringThreadLocal.set("main local var");
        System.out.println(stringThreadLocal.get());
      	testVT1.join();
        testVT2.join();
    }
}
//output
null
main local var
testVT1 local var
testVT2 local var

虚拟线程支持 ThreadLocal 的方式与平台线程相同,平台线程不能获取到虚拟线程设置的变量,虚拟线程也不能获取到平台线程设置的变量,对虚拟线程而言,负责运行虚拟线程的平台线程是透明的。但是由于虚拟线程可以创建数百万个,在虚拟线程中使用 ThreadLocal 请三思而后行。如果我们在应用程序中创建一百万个虚拟线程,那么将会有一百万个 ThreadLocal 实例以及它们引用的数据。大量的对象可能会给内存带来较大的负担。

使用 ReentrantLock 替换 Synchronized

因为 Synchronized 会使虚拟线程被固定在平台线程上,导致阻塞操作不会卸载虚拟线程,影响程序的吞吐量,所以需要使用ReentrantLock 替换 Synchronized:

befor:

public synchronized void m() {
	try {
	 	// ... access resource
	} finally {
	 	//
	}
}

after:

private final ReentrantLock lock = new ReentrantLock();

public void m() {
	lock.lock();  // block until condition holds
	try {
	 	// ... access resource
	} finally {
	 	lock.unlock();
	}
}

如何迁移

  1. 直接替换线程池为虚拟线程池。如果你的项目使用了 CompletableFuture 你也可以直接替换执行异步任务的线程池为Executors.newVirtualThreadPerTaskExecutor()。

  2. 取消池化机制。虚拟线程非常轻量级,无需池化。

  3. synchronized 改为 ReentrantLock,以减少虚拟线程被固定到平台线程。

总结

本文描述了 Java 线程模型、Java 虚拟线程的使用、原理以及适用场景,也与风靡的 Go 协程做了比较,也能找到两种实现上的相似之处,希望能帮助你理解 Java 虚拟线程。Java19 虚拟线程是预览特性,很有可能在 Java21 成为正式特性,

Java19 虚拟线程原理介绍及实现 本文介绍了 Java 线程模型、Java 虚拟线程的使用、原理和适用场景,并与流行的 Go 协程 进行了比较,也发现了两种实现方式的相似之处,希望对大家理解 Java 虚拟线程有所帮助。 阅读详情

相关推荐

Java的虚拟线程和结构化并发,含完整示例代码

Java的线程模型在云原生时代反而失去了优势,因为与操作系统1:1对应的线程模型,是一种非常重量级的线程(内存占用和上下文切换)。 核心是要重新设计线程模型和结构化并发。 Green Thread是上古Java采用过的模型,OS Thread是当前Java使用过的,Virtual Thread是正在努力进行的。 结构化并发是 2016 年才提出的新的并发编程概念。可以让你像写同步代码的方式写异步代码 Quasar,Project loom,Virtual Thread,StructuredTaskScope

技术百宝箱 1721

Java19 正式 GA,看虚拟线程如何大幅提高系统吞吐量

今天Java19 正式发布,带来了一个 Java 开发者垂涎已久的新特性—— 虚拟线程。在 Java 有这个新特性之前,Go语言的协程风靡已久,在并发编程领域可以说是叱咤风云。随着国内 Go 语言的快速发展与推广,协程好像成为了一个世界上最好语言的必备特性之一。Java19 虚拟线程就是来弥补这个空白的。本文将通过对虚拟线程的介绍,以及与 Go 协程的对比来带大家尝鲜 Java19 虚拟线程。

PP_HUANG的博客 947

【java】jdk 19 虚拟线程 简介

影响最深远的Java 19更新之一是虚拟线程的引入。虛拟线程是Project Loom的一部分,在Java 19中可以作为Preview使用。的补充,虚拟线程也叫协程。

九师兄 213

Java 19新特性:虚拟线程(Virtual Threads )

JDK19马上就要发布了,今天我们来谈一谈JDK 19的新特性虚线程,它属于JEP 425 Project Loom(425:)。这可能是JDK 19中最值得期待的预览特性之一。我们知道Java阻塞平台线程的代价很高,这就是为什么要不惜一切代价避免这种情况,以及为什么要基于回调编写响应式代码。响应式代码做得很好,但它使代码难以阅读和测试,这显然不是理想的情况。使用DK9和虚拟线程,阻塞线程变得如此廉价,以至于我们不再需要编写响应式代码,我们 的老式阻塞同步代码也可以很好地工作!

XiumingLee的博客 6584

如何在Tagetik系统中抽取处理SAP系统财务数据

如何在Tagetik系统中抽取处理不同核算系统财务数据:以SAP为例,简单讲一下Tagetik对接SAP数据的基本情况

2201_75401663的博客 578

RTAB-map:实时高效的SLAM库

Real‐time appearance‐based mapping (RTAB‐Map)是一种基于外观的闭环检测方法,具有良好的内存管理,以满足处理大场景和在线长周期管理要求。RTAB‐Map集成了视觉和激光雷达SLAM方案,并支持目前绝大多数的传感器 是一个强大的、开源的Simultaneous Localization and Mapping(SLAM)库,专为机器人和无人驾驶领域设计。它提供了一套完整的解决方案,用于构建三维地图,并在未知环境中进行实时定位与导航。 github项目地址 RTAB-

weixin_45761399的博客 1万+

Java19 虚拟线程 VS Go 协程 的吞吐量

Java19 带来了 Java 新特性 —— 虚拟线程。Java 新特性之前,Go 语言的协程在并发编程领域得到友好的口碑。随着国内 Go 语言的快速发展与推广,协程好像成为了一个世界上最好语言的必备特性之一。Java19 虚拟线程就是来弥补这个空白的。

ejinxian的专栏 2742

机器人动力学与控制学习笔记(一)————机器人控制概述

一、概述 1、机器人控制的目的 对自由运动机器人来说,控制的目的是要控制机器人手端的位置和姿态(以后统一简称为位姿),即所谓位置控制问题。这时希望机器人手端所达到的位置称为期望位置或期望轨迹。期望轨迹可以在机器人任务空间中给出,也可以通过逆运动学关系转化为机器人关节空间中的期望轨迹。期望轨迹通常有两种形式:一种是一个固定位置(即固定点),另一种是一条随时间连续变化的轨迹。自由运动机器人位置控制的目的,是要使机器人手端从任意起始位置出发都能到达期望轨迹。当期望轨迹是固定点时的位置控制称为定...

冰与火的青春的博客 6883

Java VirtualThread 虚拟线程的个人粗浅理解

上图证明虚拟线程底层依赖于ForkJoinPool(),还要注意一点ForkJoinPool()的平台线程都是守护线程与其他线程池不一致。先说结论:虚拟线程依赖于ForkJoinPool()进行底层实现,ForkJoinPool()依赖于平台线程。先随便看个VirtualThread的相关资料理解虚拟线程和平台线程的概念或者看这个也行。

学医救不了中国人 2128

Linkage Mapper 工具参数详解——Building Network and Map Linkages

此文档用于解析使用LinkageMapper各输入输出参数详情以及可能的影响,并介绍了如何解释模型输出结果和输出参数,适合刚入手的人。篇幅很长很啰嗦,是因为每个参数都解释的万分细致。

sumuses的博客 6122

【单点登录】SSO单点登录sa-token接入流程详解

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。

Hello, New World! 1795

JDK21的虚拟线程是什么?和平台线程什么关系?

关于平台线程和系统内核线程的对应关系多提一点:在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。JVM 调度程序通过平台线程(载体线程)来管理虚拟线程,一个平台线程可以在不同的时间执行不同的虚拟线程(多个虚拟线程挂载在一个平台线程上),当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。线程类,这样可以平滑的过渡到虚拟线程的使用。

JavaGuide 6989

【25届数字IC秋招总结】面试经验10——紫光同芯

25届数字ic面试记录——紫光同芯

lalala0635的博客 863

## 互联网摸鱼日报(2022-09-23)

BERT能否被“平替”?作业帮文本分类场景下的一次尝试Gartner公布2022新兴技术成熟度曲线,这些技术趋势最值得关注技术人视角下的 NFT|InfoQ大会早班车第22期云上可观测性体系建设数智底座必备能力二:以数据智能驱动业务增长企业战略制定,既要突破边界也要约束边界从一线研发到公司创始人,基础软件创业者迷雾中与市场赛跑云上OLAP引擎查询性能评估框架:设计与实现600+服务模块,1万+POD数量,作业帮从PHP迁移至Go实战总结人工智能项目到底适不适合开源?Hey,bro!来参加一场世界级的科技竞赛

weixin_43849685的博客 826

Lammps不同亲疏水性粗糙表面的润湿行为模拟(包含in文件)

Lammps粗糙表面的润湿行为模拟(包含in文件)前言—润湿行为模拟一、物理模型构建二、具体in文件—in文件内循环亲疏水性三、具体in文件—分子模板构建初始模型以及平衡态模拟四、全部代码(基地粗糙表面只提供data文件) 前言—润湿行为模拟 润湿现象:当液体与固体接触时,液体的附着层将沿固体表面延伸。当接触角 θ\rm \thetaθ 为锐角时,液体润湿固体,若θ为零时,液体将展延到全部固体表面上。润湿现象的产生与液体和固体的性质有关。(来自百度百科) 润湿行为的本质是固液之间的相互作用,也就是分子间的范

Rockyoung 7094

【CAD】【手把手带你写一个DXF&JAVA解析框架】02——整理DXF元数据实体类,涵盖重要属性与对照组码

本文系统梳理了DXF文件解析中的核心元数据类,重点分析了Tables段(图层、块记录)、Blocks段和Entities段的关键数据结构。通过详细的组码说明和对照代码实现,阐述了图层定义(DXFLayer)、块记录(DXFBlockRecord)、应用程序标识(DXFAppId)等表对象的属性构成;同时解析了直线(DXFLine)、多段线(DXFLwPolyLine)、圆弧(DXFArc)、文字(DXFText)等实体类的几何特征与属性定义。文章还构建了顶点(Vertex)、基类(DXFEntity、DXF

Wonder ZH 270

Java 19 虚拟线程

Java 19 虚拟线程介绍

DevCampsite的博客 5685

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

JDK19 - 虚拟线程详解

之前在Virtual Thread 虚拟线程探究这篇文章里面主要讲了一下虚拟线程的使用和简要的介绍。这篇文章我们就来深入学习一下相关的原理。:一种提供执行和暂停函数的服务类。Scheduler:执行器。负责将虚拟线程挂载到平台线程上。底层交给执行。其实对上面的流程进行一个简要的总结就是。针对run()先进行装载。把实例和运载线程进行绑定。判断是否存在堆栈内存空间,若存在,说明之前已经执行过一部分调用栈了。那么继续执行(倘若执行过调用栈,那么会把相关数据信息存储到堆内存中)。

Zong_0915的博客 2101

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

JAVA19新特性虚拟线程(协程)

虚拟线程的创建、调度和销毁都由 JVM 调度器处理,虚拟线程的调度独立于操作系统的线程调度,能够有效减少传统线程的开销。当虚拟线程执行 I/O 阻塞或其他长时间等待的操作时,JVM 会将该虚拟线程挂起,操作系统线程可以继续执行其他虚拟线程,避免了传统线程模型中的阻塞问题。:虚拟线程能够避免传统线程中的阻塞问题,当一个虚拟线程在进行 I/O 阻塞时,JVM 会将其挂起并调度其他虚拟线程继续执行。虚拟线程由 JVM 内部的调度器进行管理,调度器将虚拟线程分配到操作系统线程上执行。

fqlya的博客 846

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

【jdk19虚拟线程 VS 普通线程】

主要介绍下jdk19的新特性虚拟线程,真是性能优化的又一大利器啊!如果小伙伴们项目里面jdk有紧跟潮流,在合适的业务场景不妨试试吧~

weixin_44585177的博客 1421

Java19预览特性:虚拟线程

本文以Java19预览特性为引子,介绍了协程的概念,实现方案,同时本地测试了Java异步回调和协程的性能对比。

qq_44723152的博客 592

JAVA19虚拟线程以及原理

JAVA19虚拟线程

qq1063420481的博客 3986

零基础也能懂!虚拟线程让微服务网关轻松扛住高并发(附实战案例)

对小白:无需深入理解复杂的线程池原理,框架自动帮你优化对企业:用更低的成本解决高并发难题,性价比拉满现在就试试在你的网关项目中开启虚拟线程吧,让系统轻松应对流量洪峰!

全栈开发工程师 991

Java 19 新特性—虚拟线程

众所周知,JVM 是一个多线程环境,它通过为我们提供了对操作系统线程的抽象,但是 Java 中的线程都只是对操作系统线程的一种简单封装,我们可以称之为“平台线程”。代价昂贵:创建平台线程的成本很高。每当创建一个平台线程时,操作系统都必须在堆栈中分配大量内存来存储线程的上下文、原生调用堆栈和 Java 调用堆栈。由于堆栈大小是固定的,这就导致了高昂的内存开销。上下文切换成本高:在多线程环境下,需要在不同线程间切换,这种上下文切换会消耗时间和资源。线程数量有限。

smart_an的专栏 985

Java虚拟线程:告别线程池噩梦,性能提升10倍是真的吗?

Java虚拟线程(Virtual Threads)是Java 19引入的轻量级并发解决方案,旨在解决传统线程池在高并发IO密集型场景下的性能瓶颈。虚拟线程由JVM管理,创建成本低,支持数百万并发,在阻塞操作时会自动让出平台线程资源。测试显示,相比200线程的传统线程池,虚拟线程在处理10000个IO任务时性能提升4-5倍。特别适合HTTP服务、数据库查询等场景,可通过Spring Boot 3.2+直接集成。但需注意不适用于CPU密集型任务,且ThreadLocal行为可能与传统线程不同。虚拟线程简化了高并

沉淀所学,分享所思,让热爱与成长同行。商务记录AI实战经验,助力开发者快速成长。 热爱AI,希望做出有影响力的技术成果。 技术点亮生活,分享连接价值与机会。 专注AI落地实战,陪你走好技术每一步。 1万+

虚拟线程使用场景

与传统的操作系统线程(平台线程)相比,虚拟线程具有更低的内存开销和更高的调度效率,因此能够大幅提升高并发系统的性能和资源利用率。虚拟线程的优势主要体现在高并发、低资源占用以及减少上下文切换的开销。在分布式计算和任务调度系统中,虚拟线程能够有效地管理大量并发的计算任务,尤其是在任务执行过程包含大量等待(如I/O操作)时,虚拟线程可以大大提升任务的并发能力。虚拟线程可以用于大规模数据处理场景,在数据流处理和计算密集型任务中,虚拟线程可以帮助减少大量独立任务所需的线程数量,从而避免线程资源的浪费。

weixin_42020487的博客 775
上一篇: 聊聊分布式一致性
下一篇: 【项目实战】Java 贪吃蛇
java_lujj
博客等级 码龄4年 18粉丝 152原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值