java19预览版特性/java21正式版特性:Virtual Threads(虚拟线程)的发展来源/特点/创建

发展来源

虚拟线程主要作用是提升服务器端的吞吐量。
吞吐量与下面3点有关:

  • 延迟:请求处理的耗时。
  • 并发量:同一时刻处理的请求数量。在以前的java版本中,一个请求对应一个线程。
  • 吞吐量:单位时间内处理的数据数量。

比如一个服务器应用程序的延迟是50ms,处理10个并发请求,则吞吐量是200请求/秒(10 / 0.05)。如果吞吐量要提高到2000请求/秒,则处理的并发请求数量要提高到100。按照1个请求对应一个线程的比例来看,线程数量也要增加到100。

但java中的线程是在操作系统线程里进行了一层包装,而操作系统中线程与硬件相关,不能轻易增加,此时线程数量就限制了系统性能。当遇到以下场景时,系统很可能卡顿:

  • 至少几千的并发任务量
  • 任务为io密集型
    为了解决该问题,虚拟线程就出现了。

原先的对应情况:
java19预览版特性/java21正式版特性:Virtual Threads(虚拟线程)的发展来源/特点/创建_第1张图片

现在的对应情况:
java19预览版特性/java21正式版特性:Virtual Threads(虚拟线程)的发展来源/特点/创建_第2张图片

特点

  • 虚拟线程提高了系统的吞吐量。
  • 虚拟线程创建所耗费的资源是极低的,无需系统调用和系统级别的上下文切换
  • 虚拟线程的生命周期短暂,不会有很深的栈的调用
  • 一个虚拟线程的生命周期中只运行一个任务,因此我们可以创建大量的虚拟线程
  • 虚拟线程无需池化。

简单应用

//ExecutorService实现了AutoCloseable接口,可以自动关闭了
try (ExecutorService executor = Executors.newCachedThreadPool()) {
    //向executor中提交1000000个任务
    IntStream.range(0, 1000000).forEach(
        i -> {
            executor.submit(() -> {
                try {
                    //睡眠1秒,模拟耗时操作
                    Thread.sleep(Duration.ofSeconds(1));
                    System.out.println("执行任务:" + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });
        });
} catch (Exception e) {
    e.printStackTrace();
}

以上代码中为每个任务创建一个线程,当任务量较多的时候,你的电脑可以感受到明显的卡顿(如果没有,可以增加任务数量试下)。
而如果将newCachedThreadPool改为newVirtualThreadPerTaskExecutor,就不会感到卡顿了。

调度器

以前线程和任务之间的关系:
java19预览版特性/java21正式版特性:Virtual Threads(虚拟线程)的发展来源/特点/创建_第3张图片
使用虚拟线程之后,任务-虚拟线程-调度器-线程的关系,1个平台线程可以被调度器分配不同的虚拟线程:
java19预览版特性/java21正式版特性:Virtual Threads(虚拟线程)的发展来源/特点/创建_第4张图片

携带器

调度器将虚拟线程挂载到线程之后,该线程叫做虚拟线程的携带器,在一个虚拟线程的生命周期中可以被分配到不同的携带器,即虚拟线程运行了一小段代码后,可能会脱离携带器,此时其他的虚拟线程会被分配到这个携带器上。

携带器和虚拟线程是相互独立的,比如:

  • 虚拟线程不能使用携带器的标识,Thread.current()方法获取的是虚拟线程本身。
  • 两者有各自的栈空间。
  • 两者不能访问对方的Thread Local变量。

在程序的执行过程中,虚拟线程遇到阻塞的操作时大部分情况下会被解除挂载,阻塞结束后,虚拟线程会被调度器重新挂载到携带器上,因此虚拟线程会频繁的挂载和解除挂载,这并不会导致操作系统线程的阻塞。譬如get方法和send方法(会有io操作)时会使虚拟线程发生挂载和解除挂载。

下面情况不会导致虚拟线程的解除挂载,会同时阻塞携带器和操作系统线程:

  • 执行synchronized同步代码(会导致携带器阻塞,所以建议使用ReentrantLock替换掉synchronized)
  • 执行本地方法或外部函数
  • 操作系统基本的文件操作
  • java中的Object.wait()方法

创建虚拟线程

java中创建的虚拟线程本质都是通过Thread.Builder.OfVirtual对象进行创建的。

Thread.startVirtualThread

1.通过Thread.startVirtualThread直接创建一个虚拟线程

//创建任务
Runnable task = () -> {
    System.out.println("执行任务");
};

//创建虚拟线程将任务task传入并启动
Thread.startVirtualThread(task);

//主线程睡眠,否则可能看不到控制台的打印
TimeUnit.SECONDS.sleep(1);

Thread.ofVirtual()方法

2.使用Thread.ofVirtual()方法创建

//创建任务
Runnable task = () -> {
    System.out.println(Thread.currentThread().getName());
};

//创建虚拟线程命名为诺手,将任务task传入
Thread vt1 = Thread.ofVirtual().name("诺手").unstarted(task);
vt1.start();//启动虚拟线程

//主线程睡眠,否则可能看不到控制台的打印
TimeUnit.SECONDS.sleep(1);

也可以在创建虚拟线程的时候直接启动,
即将

Thread vt1 = Thread.ofVirtual().name("诺手").unstarted(task);
vt1.start();

换为

Thread vt1 = Thread.ofVirtual().name("诺手").start(task);

ExecutorService

3.通过ExecutorService创建,为每个任务分配一个虚拟线程,下面代码中提交了100个任务,对应会有100个虚拟线程进行处理。

/*
    通过ExecutorService创建虚拟线程
    ExecutorService实现了AutoCloseable接口,可以自动关闭了
*/
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    //向executor中提交100个任务
    IntStream.range(0, 100).forEach(i -> {
        executor.submit(() -> {
            //睡眠1秒
            try {
                Thread.sleep(Duration.ofSeconds(1));
                System.out.println(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }                    
        });
    });
}  

你可能感兴趣的:(服务器,运维,java,后端)