Java虚拟线程探究与性能解析
阿里妹导读
本文主要介绍了阿里云在Java-虚拟-线程任务中的新进展和技术细节。
一、背景
一些希望充分利用硬件的开发人员放弃了thread-per-request的形式,转而采用响应式编程。即请求处理代码不是从头到尾都在一个线程上进行,而是在等待 I/O 操作完成时将其线程返回到池中,以便线程可以为其他请求提供服务。这种细粒度的线程共享(在这种共享中,代码仅在线程执行计算时保留在线程上,而不是在等待 I/O 时保留线程)允许大量并发操作,而不会长时间占用线程。
二、实现
定义
原理
final class VirtualThread extends BaseVirtualThread {private static final ForkJoinPool DEFAULT_SCHEDULER = createDefaultScheduler();private final Executor scheduler;private final Continuation cont;private final Runnable runContinuation;private volatile Thread carrierThread;VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {super(name, characteristics, /*bound*/ false);Objects.requireNonNull(task);// choose scheduler if not specifiedif (scheduler == null) {Thread parent = Thread.currentThread();if (parent instanceof VirtualThread vparent) {scheduler = vparent.scheduler;} else {scheduler = DEFAULT_SCHEDULER;}}this.scheduler = scheduler;this.cont = new VThreadContinuation(this, task);this.runContinuation = this::runContinuation;}private static ForkJoinPool createDefaultScheduler() {ForkJoinWorkerThreadFactory factory = pool -> {PrivilegedAction<ForkJoinWorkerThread> pa = () -> new CarrierThread(pool);return AccessController.doPrivileged(pa);};PrivilegedAction<ForkJoinPool> pa = () -> {int parallelism, maxPoolSize, minRunnable;String parallelismValue = System.getProperty("jdk.virtualThreadScheduler.parallelism");String maxPoolSizeValue = System.getProperty("jdk.virtualThreadScheduler.maxPoolSize");String minRunnableValue = System.getProperty("jdk.virtualThreadScheduler.minRunnable");if (parallelismValue != null) {parallelism = Integer.parseInt(parallelismValue);} else {parallelism = Runtime.getRuntime().availableProcessors();}if (maxPoolSizeValue != null) {maxPoolSize = Integer.parseInt(maxPoolSizeValue);parallelism = Integer.min(parallelism, maxPoolSize);} else {maxPoolSize = Integer.max(parallelism, 256);}if (minRunnableValue != null) {minRunnable = Integer.parseInt(minRunnableValue);} else {minRunnable = Integer.max(parallelism / 2, 1);}Thread.UncaughtExceptionHandler handler = (t, e) -> { };boolean asyncMode = true; // FIFOreturn new ForkJoinPool(parallelism, factory, handler, asyncMode,0, maxPoolSize, minRunnable, pool -> true, 30, SECONDS);};return AccessController.doPrivileged(pa);}private void runContinuation() {// the carrier must be a platform threadif (Thread.currentThread().isVirtual()) {throw new WrongThreadException();}// set state to RUNNINGint initialState = state();if (initialState == STARTED || initialState == UNPARKED || initialState == YIELDED) {// newly started or continue after parking/blocking/Thread.yieldif (!compareAndSetState(initialState, RUNNING)) {return;}// consume parking permit when continuing after parkingif (initialState == UNPARKED) {setParkPermit(false);}} else {// not runnablereturn;}mount();try {cont.run();} finally {unmount();if (cont.isDone()) {afterDone();} else {afterYield();}}}}
Continuation:译为“续延”,是用户真实任务的包装器,虚拟线程会把任务包装到一个Continuation实例中,当任务需要阻塞挂起的时候,会调用Continuation的yield操作进行阻塞
Scheduler:译为“调度器”,会把任务提交到一个平台线程池中执行,虚拟线程中维护了一个默认的调度器DEFAULT_SCHEDULER,这是一个 ForkJoinPool 实例,最大线程数默认是系统核心线程数,最大为 256,可以通过 jdk.virtualThreadScheduler.maxPoolSize 进行设置。
carrier:载体线程(Thread对象),指的是负责执行虚拟线程中任务的平台线程。
runContinuation:一个Runnable对象,用于在任务运行或继续之前,虚拟线程将装载到当前线程上。当任务完成或完成时,将其卸载。
三、使用
使用Thread类创建虚拟线程
// name(String prefix, Integer start) p0:前缀 p1:计数器初始值Thread.Builder.OfVirtual virtualThreadBuilder = Thread.ofVirtual().name("worker-", 0);Thread worker0 = virtualThreadBuilder.start(this::doSomethings);worker0.join();System.out.print("finish worker-0 running");Thread worker1 = virtualThreadBuilder.start(this::doSomethings);worker1.join();System.out.print("finish worker-1 running");
使用Executors创建虚拟线程
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {Future<?> submit = executorService.submit(this::doSomethings);submit.get();System.out.print("finish running");}
使用虚拟线程实现服务端
public class Server {public static void main(String[] args) {Set<String> platformSet = new HashSet<>();new Thread(() -> {try {Thread.sleep(10000);System.out.println(platformSet.size());} catch (InterruptedException e) {throw new RuntimeException(e);}}).start();try (ServerSocket serverSocket = new ServerSocket(9999)) {Thread.Builder.OfVirtual clientThreadBuilder = Thread.ofVirtual().name("client", 1);while (true) {Socket clientSocket = serverSocket.accept();clientThreadBuilder.start(() -> {String platformName = Thread.currentThread().toString().split("@")[1];platformSet.add(platformName);try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);) {String inputLine;while ((inputLine = in.readLine()) != null) {System.out.println(inputLine + "(from:" + Thread.currentThread() + ")");out.println(inputLine);}} catch (IOException e) {System.err.println(e.getMessage());}});}} catch (IOException e) {System.err.println("Exception caught when trying to listen on port 999");System.err.printf(e.getMessage());}}}
public class Client {public static void main(String[] args) throws InterruptedException {Thread.Builder.OfVirtual builder = Thread.ofVirtual().name("client", 1);for (int i = 0; i < 100000; i++) {builder.start(() -> {try (Socket serverSocket = new Socket("localhost", 9999);BufferedReader in = new BufferedReader(new InputStreamReader(serverSocket.getInputStream()));PrintWriter out = new PrintWriter(serverSocket.getOutputStream(), true);) {out.println("hello");String inputLine;while ((inputLine = in.readLine()) != null) {System.out.println(inputLine);}} catch (UnknownHostException e) {System.err.println("Don't know about localhost");} catch (IOException e) {System.err.println("Couldn't get I/O for the connection to localhost");}});}Thread.sleep(1000000000);}}
虚拟线程调度和固定
1. 虚拟线程执行的方法或块被 synchronized 关键字标识时;
固定不会使应用程序出错,但可能会阻碍其可伸缩性。可以尝试通过使用:
java.util.concurrent.locks.ReentrantLock.synchronized
四、注意事项
写简单的同步代码,使用阻塞的API
CompletableFuture.supplyAsync(info::getUrl, pool).thenCompose(url -> getBodyAsync(url, HttpResponse.BodyHandlers.ofString())).thenApply(info::findImage).thenCompose(url -> getBodyAsync(url, HttpResponse.BodyHandlers.ofByteArray())).thenApply(info::setImageData).thenAccept(this::process).exceptionally(t -> { t.printStackTrace(); return null; });
try {String page = getBody(info.getUrl(), HttpResponse.BodyHandlers.ofString());String imageUrl = info.findImage(page);byte[] data = getBody(imageUrl, HttpResponse.BodyHandlers.ofByteArray());info.setImageData(data);process(info);} catch (Exception ex) {t.printStackTrace();}
不要共用虚拟线程
Future<ResultA> f1 = sharedThreadPoolExecutor.submit(task1);Future<ResultB> f2 = sharedThreadPoolExecutor.submit(task2);// ... use futures
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {Future<ResultA> f1 = executor.submit(task1);Future<ResultB> f2 = executor.submit(task2);// ... use futures}
代码仍然使用ExecutorService,但是
void handle(Request request, Response response) {var url1 = ...var url2 = ...try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {var future1 = executor.submit(() -> fetchURL(url1));var future2 = executor.submit(() -> fetchURL(url2));response.send(future1.get() + future2.get());} catch (ExecutionException | InterruptedException e) {response.fail(e);}}String fetchURL(URL url) throws IOException {try (var in = url.openStream()) {return new String(in.readAllBytes(), StandardCharsets.UTF_8);}}
使用信号量限制并发
Semaphore sem = new Semaphore(10);// ......Executors.newVirtualThreadPerTaskExecutor().submit(() -> {try {// 执行任务前信号量-1,表示多了一个并发线程在执行了,剩下的可同时执行数量减少// 如果信号量(许可证)为0,则阻塞直到有其他线程执行完成释放许可证sem.acquire();doSomething();} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 执行完任务后信号量+1sem.release();}});
不要在线程局部变量中缓存昂贵的可重用对象
避免长时间和频繁的固定
synchronized(lockObj) {frequentIO();}
lock.lock();try {frequentIO();} finally {lock.unlock();}
五、性能测试
平台线程与虚拟线程简单对比示例
public class PerformanceTest {private static final int REQUEST_NUM = 10000;public static void main(String[] args) {long vir = 0, p1 = 0, p2 = 0, p3 = 0, p4 = 0;for (int i = 0; i < 3; i++) {vir += testVirtualThread();p1 += testPlatformThread(200);p2 += testPlatformThread(500);p3 += testPlatformThread(800);p4 += testPlatformThread(1000);System.out.println("--------------");}System.out.println("虚拟线程平均耗时:" + vir / 3 + "ms");System.out.println("平台线程[200]平均耗时:" + p1 / 3 + "ms");System.out.println("平台线程[500]平均耗时:" + p2 / 3 + "ms");System.out.println("平台线程[800]平均耗时:" + p3 / 3 + "ms");System.out.println("平台线程[1000]平均耗时:" + p4 / 3 + "ms");}private static long testVirtualThread() {long startTime = System.currentTimeMillis();ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();for (int i = 0; i < REQUEST_NUM; i++) {executorService.submit(PerformanceTest::handleRequest);}executorService.close();long useTime = System.currentTimeMillis() - startTime;System.out.println("虚拟线程耗时:" + useTime + "ms");return useTime;}private static long testPlatformThread(int poolSize) {long startTime = System.currentTimeMillis();ExecutorService executorService = Executors.newFixedThreadPool(poolSize);for (int i = 0; i < REQUEST_NUM; i++) {executorService.submit(PerformanceTest::handleRequest);}executorService.close();long useTime = System.currentTimeMillis() - startTime;System.out.printf("平台线程[%d]耗时:%dms\n", poolSize, useTime);return useTime;}private static void handleRequest() {try {Thread.sleep(300);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
虚拟线程耗时:654ms平台线程[200]耗时:15551ms平台线程[500]耗时:6241ms平台线程[800]耗时:4069ms平台线程[1000]耗时:3137ms--------------虚拟线程耗时:331ms平台线程[200]耗时:15544ms平台线程[500]耗时:6227ms平台线程[800]耗时:4047ms平台线程[1000]耗时:3126ms--------------虚拟线程耗时:326ms平台线程[200]耗时:15552ms平台线程[500]耗时:6228ms平台线程[800]耗时:4054ms平台线程[1000]耗时:3151ms--------------虚拟线程平均耗时:437ms平台线程[200]平均耗时:15549ms平台线程[500]平均耗时:6232ms平台线程[800]平均耗时:4056ms平台线程[1000]平均耗时:3138ms
简单的Web服务测试
@RestControllerpublic class TestController {@GetMapping("get")public String get() {try {// System.out.println(Thread.currentThread());Thread.sleep(300);} catch (InterruptedException e) {throw new RuntimeException(e);}return "ok";}}
server:tomcat:threads:max: 200spring:threads:virtual:enabled: false # 是否启用虚拟线程
@Configurationpublic class TestWebClient {@Beanpublic RouterFunction<ServerResponse> routes() {return route(GET("/get"),request -> ok().contentType(MediaType.APPLICATION_JSON).body(fromPublisher(Mono.just("ok").delayElement(Duration.ofMillis(300)), String.class)));}}
六、结语
参考资料:
Virtual Threads :
https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-DC4306FC-D6C1-4BCC-AECE-48C32C1A8DAA
JEP 444: Virtual Threads :
https://openjdk.org/jeps/444#Thread-local-variables
Spring Webflux :
https://springdoc.cn/spring-webflux/
使用 GPU 共享推理一键部署
通过创建ACK集群Pro版,使用云原生AI套件提交模型微调训练任务与部署GPU共享推理服务。支持快速创建Kubernetes集群,白屏配置任务数据共享存储和下载,并通过命令行工具Arena快速提交模型训练任务、部署推理服务。
点击阅读原文查看详情。