Java多线程基础:虚拟线程与平台线程解析

发布时间:2024年01月16日

在这篇文章中,主要总结一些关于线程的概念,以及更近期的名为虚拟线程的特性。将了解平台线程和虚拟线程在性质上的区别,以及它们如何促进应用程序性能的改进
在这里插入图片描述

经典线程背景:

让我们以调用外部API或某些数据库交互的场景为例,看看线程执行的生命周期。

  1. 线程被创建并准备在内存中提供服务。
  2. 一旦请求到达,它被映射到其中一个线程,然后通过调用外部API或执行某些数据库查询来提供服务。
  3. 线程等待,直到它从服务或数据库获取到响应。
  4. 一旦收到响应,它执行后响应的活动并返回到线程池。

在这里插入图片描述
观察上述生命周期中的第3步,即线程只是等待且什么都不做。这是一个主要的缺点,通过仅等待而未充分利用系统资源,大多数线程在其生命周期中只是等待响应而无所作为。

在Java 19或更高版本之前,创建线程或现有线程的标准方式被称为本地线程或平台线程。在这种架构风格中,平台线程与操作系统线程之间存在一对一的映射。这意味着操作系统线程被低效使用,因为它只是等待活动完成而无所作为,从而使它们变得沉重且昂贵。

虚拟线程:

Java中的虚拟线程代表了Java处理并发和多线程的重要演变。作为Oracle的项目Loom的一部分引入,该项目是Oracle解决编写、维护和观察高吞吐并发应用程序所面临挑战的倡议。虚拟线程被设计为轻量级,并使并发对开发人员更加容易。

虚拟线程是由Java虚拟机(JVM)管理的轻量级线程,而不是由操作系统管理。与平台线程不同,虚拟线程创建和销毁成本较低。它们映射到较少的平台线程,使Java应用程序能够以更低的资源占用同时处理数千甚至数百万个任务。

在这里插入图片描述

虚拟线程在平台线程上的有用性

虚拟线程相对于平台线程的优点是多方面的。首先,它们能够更有效地利用系统资源。由于虚拟线程轻量级,与平台线程相比,它们消耗更少的内存和CPU资源。这种效率允许更高程度的并发,使得能够在单个JVM上运行大量并发任务。

其次,虚拟线程简化了Java中的并发编程。它们允许开发人员以直观、命令式的风格编写代码,类似于编写同步代码的方式,而不必处理异步编程模型的复杂性。这种简单性降低了常见的与并发相关的错误的可能性,如死锁和竞争条件。

此外,虚拟线程促进了更好的CPU利用率。在传统的线程模型中,大量的CPU时间可能会在管理和在许多线程之间进行上下文切换方面浪费。虚拟线程减少了与上下文切换相关的开销,允许更有效地执行并发任务。

实际应用:

如果我们需要创建经典的平台线程来完成任务,我们可以按照以下步骤操作。创建一个名为PlatformThreadDemo.java的文件,并将内容复制如下。

package org.vaslabs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PlatformThreadDemo {
    private static final Logger logger = LoggerFactory.getLogger(PlatformThreadDemo.class);

    public static void main(String[] args) {
        attendMeeting().start();
        completeLunch().start();
    }

    private static Thread attendMeeting(){
        var message = "Platform Thread [Attend Meeting]";
        return new Thread(() -> {
            logger.info(STR."{} | \{message}", Thread.currentThread());
        });
    }

    private static Thread completeLunch(){
        var message = "Platform Thread [Complete Lunch]";
        return new Thread(() -> {
            logger.info(STR."{} | \{message}", Thread.currentThread());
        });
    }

    // using builder pattern to create platform threads
    private static void attendMeeting1(){
        var message = "Platform Thread [Attend Meeting]";
        Thread.ofPlatform().start(() -> {
            logger.info(STR."{} | \{message}", Thread.currentThread());
        });
    }

    private static void completeLunch1(){
        var message = "Platform Thread [Complete Lunch]";
        Thread.ofPlatform().start(() -> {
            logger.info(STR."{} | \{message}", Thread.currentThread());
        });
    }
}

上述示例展示了两种创建平台线程的方法:

  1. 使用Thread构造函数并将可运行的lambda传递给它。
  2. 使用Thread的Platform()构建方法。
    让我们通过创建一些并发的虚拟线程来看更复杂的编码。创建一个名为DailyRoutineWorkflow.java的文件,并将下面的代码复制到其中。
package org.vaslabs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

public class DailyRoutineWorkflow {
    static final Logger logger = LoggerFactory.getLogger(DailyRoutineWorkflow.class);

    static void log(String message) {
        logger.info(STR."{} | \{message}", Thread.currentThread());
    }


    private static void sleep(Long duration) throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(duration);
    }

    private static Thread virtualThread(String name, Runnable runnable) {
        return Thread.ofVirtual().name(name).start(runnable);
    }

    static Thread attendMorningStatusMeeting() {
        return virtualThread(
                "Morning Status Meeting",
                () -> {
                    log("I'm going to attend morning status meeting");
                    try {
                        sleep(1000L);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    log("I'm done with morning status meeting");
                });
    }

    static Thread workOnTasksAssigned() {
        return virtualThread(
                "Work on the actual Tasks",
                () -> {
                    log("I'm starting my actual work on tasks");
                    try {
                        sleep(1000L);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    log("I'm done with actual work on tasks");
                });
    }

    static Thread attendEveningStatusMeeting() {
        return virtualThread(
                "Evening Status Meeting",
                () -> {
                    log("I'm going to attend evening status meeting");
                    try {
                        sleep(1000L);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    log("I'm done with evening status meeting");
                });
    }

    static void concurrentRoutineExecutor() throws InterruptedException {
        var morningMeeting = attendMorningStatusMeeting();
        var actualWork = workOnTasksAssigned();
        var eveningMeeting = attendEveningStatusMeeting();
        morningMeeting.join();
        actualWork.join();
        eveningMeeting.join();
    }
}

上述代码展示了使用工厂方法创建虚拟线程。除了工厂方法之外,我们还可以使用专为虚拟线程定制的java.util.concurrent.ExecutorService来实现虚拟线程,称为java.util.concurrent.ThreadPerTaskExecutor。您可以使用ExecutorService获得与上述相同的功能,如下所示。

package org.vaslabs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class DailyRoutineWorkflowUsingExecutors {
    static final Logger logger = LoggerFactory.getLogger(DailyRoutineWorkflowUsingExecutors.class);

    static void log(String message) {
        logger.info(STR."{} | \{message}", Thread.currentThread());
    }


    private static void sleep(Long duration) throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(duration);
    }

    public static void executeJobRoute() throws ExecutionException, InterruptedException {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var morningMeeting = executor.submit(() -> {
                log("I'm going to attend morning status meeting");
                try {
                    sleep(1000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                log("I'm done with morning status meeting");
            });

            var actualWork = executor.submit(() -> {
                log("I'm starting my actual work on tasks");
                try {
                    sleep(1000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                log("I'm done with actual work on tasks");
            });

            var eveningMeeting = executor.submit(() -> {
                log("I'm going to attend evening status meeting");
                try {
                    sleep(1000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                log("I'm done with evening status meeting");
            });

            morningMeeting.get();
            actualWork.get();
            eveningMeeting.get();
        }
    }
}

深入了解输出

如果您运行上述代码,无论是使用工厂方法还是ExecutorServices,您将看到类似于以下的输出。
在这里插入图片描述
仔细观察信息日志,您将看到“|”(管道符号)两侧的两个部分,第一部分解释了有关虚拟线程的信息,如VirtualThread[#26]/runnable@ForkJoinPool-1-worker-3。这告诉我们VirtualThread[#26]映射到平台线程runnable@ForkJoinPool-1-worker-3,而另一部分是日志的信息部分。

ThreadLocals和虚拟线程:

在Java中,ThreadLocal是一种机制,允许变量基于每个线程进行存储。访问ThreadLocal变量的每个线程都会获得其自己独立初始化的变量副本,可以在不影响其他线程中相同变量的情况下进行访问和修改。这在您想要保持特定于线程的状态(例如用户会话或数据库连接)的情景中特别有用。

然而,当与作为Loom项目的一部分引入的虚拟线程一起使用时,ThreadLocal的行为发生了显著变化。虚拟线程是由Java虚拟机(JVM)管理的轻量级线程,设计用于大量调度,而不同于与操作系统的线程管理相关联的传统平台线程。

由于可以创建数百万个虚拟线程,使用ThreadLocal可能导致内存泄漏。

package org.vaslabs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

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

    static final Logger logger = LoggerFactory.getLogger(DailyRoutineWorkflow.class);

    static void log(String message) {
        logger.info(STR."{} | \{message}", Thread.currentThread());
    }

    private static void sleep(Long duration) throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(duration);
    }

    public static void virtualThreadContext() throws InterruptedException {
        var virtualThread1 = Thread.ofVirtual().name("thread-1").start(() -> {
            stringThreadLocal.set("thread-1");
            try {
                sleep(1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log(STR."thread name is \{stringThreadLocal.get()}");
        });
        var virtualThread2 = Thread.ofVirtual().name("thread-2").start(() -> {
            stringThreadLocal.set("thread-2");
            try {
                sleep(1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log(STR."thread name is \{stringThreadLocal.get()}");
        });
        virtualThread1.join();
        virtualThread2.join();
    }
}

总结:

虚拟线程成为一个颠覆性的变革者,提供了轻量级、高效的并发性,与平台线程资源密集型的特性形成鲜明对比。它们通过在最小资源开销下使大量并发任务成为可能,从而改变了Java处理多线程的方式,简化了编程模型并增强了应用程序的可扩展性。

然而,在这个新背景下对ThreadLocal的使用的复杂性突显了需要谨慎考虑的必要性。虽然ThreadLocal在传统线程中保持特定于线程的数据方面仍然是一个强大的工具,但在虚拟线程中,它的应用变得更加复杂,需要替代策略来进行状态和上下文管理。这些概念共同标志着Java并发范式的重大转变,为开发人员构建更具响应性、可扩展性和高效性的应用程序打开了新的大门。

文章来源:https://blog.csdn.net/Kenard/article/details/135634271
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。