在这篇文章中,主要总结一些关于线程的概念,以及更近期的名为虚拟线程的特性。将了解平台线程和虚拟线程在性质上的区别,以及它们如何促进应用程序性能的改进
经典线程背景:
让我们以调用外部API或某些数据库交互的场景为例,看看线程执行的生命周期。
- 线程被创建并准备在内存中提供服务。
- 一旦请求到达,它被映射到其中一个线程,然后通过调用外部API或执行某些数据库查询来提供服务。
- 线程等待,直到它从服务或数据库获取到响应。
- 一旦收到响应,它执行后响应的活动并返回到线程池。
观察上述生命周期中的第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());
});
}
}
上述示例展示了两种创建平台线程的方法:
- 使用Thread构造函数并将可运行的lambda传递给它。
- 使用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并发范式的重大转变,为开发人员构建更具响应性、可扩展性和高效性的应用程序打开了新的大门。