Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Are Virtual Threads Going to Make Reactive Prog...

José
February 05, 2025

Are Virtual Threads Going to Make Reactive Programming Irrelevant?

Java 21 was released about a year ago, and among all the features that this version brings, Loom virtual threads are probably the most exciting and promising one. One of the promise was to enable the "simple thread-per-request style to scale with near-optimal hardware utilization", something that could only be achieved by reactive style programming. How can virtual threads achieve this kind of performance? Can virtual threads make the asynchronous programming model obsolete? Is this model going to disappear? These are the questions we cover in this presentation. Virtual threads are cheap to create, to a point where you can have as many as you need. It allows for a new API, Structured Concurrency, that brings a new asychronous programming model, simpler than the reactive programming model. The last element you need to create complete applications are Scoped Values, a replacement of Thread local variables, that we also cover.

José

February 05, 2025
Tweet

More Decks by José

Other Decks in Programming

Transcript

  1. Are Virtual Threads Going to Make Reactive Programming Irrelevant? Loom,

    Structured Concurrency, Reactive Programming José Paumard Java Developer Advocate Java Platform Group
  2. 2/5/2025 Copyright © 2023, Oracle and/or its affiliates 4 Tune

    in! Inside Java Newscast JEP Café Road To 21 series Inside.java Inside Java Podcast Sip of Java Cracking the Java coding interview
  3. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 12 As

    Well as Scoped Values http://jdk.java.net/loom/
  4. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 14 Virtual

    Threads and Reactive Programming are about solving the same problem Reactive Programming has been there for 10+ years At least you know how it is working! Virtual Threads & Structured Concurrency is not there yet Structured Concurrency is still a preview feature And it looks promising! What does it mean to be irrelevant?
  5. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 15 Concurrency

    is hard It is needed to achieve high performance (throughput) As of now, the paradigm to achieve high throughput is callback-based reactive programming … which comes with drawbacks Why do you need reactive programming? Loom is About Fixing Concurrency Issues
  6. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 16 Reading

    images and links Too unefficient to be used A Simple Example var image = someService.readImage(); var links = someService.readLinks(); var page = new Page(image, links);
  7. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 17 The

    2004 solution (with lambdas and var) What is wrong with this code? Concurrency Issues ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS));
  8. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 18 1)

    It blocks threads Blocking a platform thread is wrong, for many reasons (Here it is blocked for ~100ms) Concurrency Issues ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -> ES thread is blocked -> ES thread is blocked -> the main thread is blocked
  9. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 19 2)

    It can lead to a loose thread Concurrency Issues ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS));
  10. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 20 2)

    It can lead to a loose thread Concurrency Issues ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -> if f1.get() crashes...
  11. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 21 2)

    It can lead to a loose thread If something goes wrong in readLinks, then the thread running it is never freed. You now have a loose thread! Concurrency Issues ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -> if f1.get() crashes... ... f2.get() is never called
  12. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 22 3)

    It’s hard to debug Stack traces do not give you the information you need. Concurrency Issues ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS));
  13. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 23 3

    issues to fix: - Blocking a platform thread is bad - Having a non-relevant stack trace is annoying - Having loose threads is annoying Concurrency Issues
  14. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 24 A

    java.lang.Thread is a wrapper on a kernel (or platform) thread It needs: - ~1ms to start - ~2MB of memory to store its stack - context switching costs ~0,1ms You can only have several thousands of them What is a Platform Thread?
  15. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 25 Handling

    one network request per thread Limits your application to ~4k concurrent requests If handling one request consumes 1s of CPU time Then your CPU can handle 1M requests per second The model one request per thread does not work with Platform Threads (anymore) Why is it Wrong to Block a P. Thread?
  16. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 26 There

    are two solutions: - The reactive programming solution: have a platform thread handle many requests - How many? ~1k per P. Thread What Solution?
  17. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 27 This

    is the classical way of writing it Let’s Rewrite the Previous Code ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS));
  18. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 28 This

    is the CompletableFuture way of writing it Let’s Rewrite the Previous Code var cf1 = CompletableFuture .supplyAsync(someService::readImages) .whenComplete((images, error) -> { if (error != null) { // log something } }); var cf2 = // same with readLinks var cf3 = cf1.runAfterBoth(cf2, () -> { var page = new Page(cf1.get(), cf2.get()); // do something with page });
  19. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 29 One

    thing: never write blocking code in your lambdas! How Is Your Platform Thread Used? P. Thread
  20. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 30 Suppose

    something wrong happens in readImages() Your stack trace tells you who called readImages() What About Debug? 27: var images = someService.readImages(); Exception in thread "main" java.lang.IllegalStateException: Boom! at org.myapp.SomeService.readImages(SomeService.java:12) at org.myapp.MyApp.main(MyApp.java:27)
  21. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 31 Suppose

    something wrong happens in readImages() What About Debug? Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Boom! at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122) at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191) at org.myapp.MyApp.main(MyApp.java:32) Caused by: java.lang.IllegalStateException: Boom! at org.myapp.SomeService.readImages(SomeService.java:12) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) at java.base/java.lang.Thread.run(Thread.java:1575) 29: var f1 = es.submit(someService::readImages); 30: var f2 = es.submit(someService::readLinks); 31: 32: var p = new Page(f1.get(), f2.get());
  22. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 32 Suppose

    something wrong happens in readImages() What About Debug? var cf1 = CompletableFuture.supplyAsync(SomeService::readImages) .whenComplete((images, error) -> { if (error != null) { error.printStackTrace(); error.getCause().printStackTrace(); } }); var cf2 = // the same with readLinks cf1.runAfterBoth(cf2, () -> { try { var page = new Page(cf1.get(), cf2.get()); // do something with page } catch (Exception e) { // do something with the error } });
  23. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 33 Suppose

    something wrong happens in readImages() What About Debug? java.util.concurrent.CompletionException: java.lang.IllegalStateException: Boom! at java.base/java.util.concurrent.CompletableFuture.wrapInCompletionException(CompletableFuture.java:323) at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:359) at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:364) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1814) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1804) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:507) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1458) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:2034) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:189) var cf1 = CompletableFuture.supplyAsync(SomeService::readImages) .whenComplete((images, error) -> { if (error != null) { error.printStackTrace(); error.getCause().printStackTrace(); } });
  24. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 34 Suppose

    something wrong happens in readImages() What About Debug? java.lang.IllegalStateException: Boom! at org.paumard.SomeService.readImages(SomeService.java:12) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1812) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1804) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:507) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1458) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:2034) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:189) var cf1 = CompletableFuture.supplyAsync(SomeService::readImages) .whenComplete((images, error) -> { if (error != null) { error.printStackTrace(); error.getCause().printStackTrace(); } });
  25. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 35 -

    Efficient - The programming model is hard to read / write / test - Impossible to debug - Impossible to profile - Don’t write blocking lambdas Async Programming
  26. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 36 There

    are two solutions: - The reactive programming solution: have a platform thread handle many requests - How many? ~1k per P. Thread - Build a model of thread lighter than a Platform Thread - How much lighter? ~1k lighter Being light is not enough! What Solution?
  27. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 37 -

    To be as efficient (same throughput) - To provide a simple programming model - To make debugging possible - To make profiling possible Virtual Threads Need…
  28. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 38 There

    are two solutions: - The async programming solution: have a platform thread handle many requests - How many? ~1k per P. Thread - Build a model of thread lighter than a Platform Thread - How much lighter? ~1k lighter What Solution?
  29. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 39 A

    Virtual Thread runs on a Carrier Thread How Are Virtual Threads Working? heap Worker 1 start() Virtual thread fork join pool
  30. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 40 When

    it blocks… How Are Virtual Threads Working? heap start() blocked! Virtual thread Worker 1 fork join pool
  31. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 41 …

    its stack is moved to the heap How Are Virtual Threads Working? heap start() blocked! Virtual thread Worker 1 fork join pool
  32. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 42 …until

    the OS signals that the data is available How Are Virtual Threads Working? heap start() Virtual thread Worker 1 fork join pool
  33. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 43 …

    at which point it’s moved back… How Are Virtual Threads Working? heap start() Virtual thread Worker 1 fork join pool
  34. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 44 …

    to the same platform thread… How Are Virtual Threads Working? heap start() Virtual thread Worker 1 fork join pool
  35. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 45 …

    or to another one if it’s busy How Are Virtual Threads Working? heap Virtual thread ? Worker 1 fork join pool Worker 2
  36. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 46 There

    are two solutions: - The async programming solution: have a platform thread handle many requests - How many? ~1k per P. Thread - Build a model of thread lighter than a Platform Thread - How much lighter? ~1k lighter - Blocking a virtual thread does not block the underlying Platform Thread What Solution?
  37. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 47 So

    what is the difference with the reactive model? How Is Your Platform Thread Used? P. Thread P. Thread
  38. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 48 So

    what is the difference with the reactive model? How Is Your Platform Thread Used? P. Thread P. Thread
  39. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 49 The

    code you write is the most basic code you can write Virtual threads handle the blocking API for you Writing blocking code is OK (Actually, this is what you should do) Virtual Threads Programming Model var images = someService.readImages();
  40. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 50 The

    code you write is the most basic code you can write Virtual threads handle the blocking API for you Writing blocking code is OK (Actually, this is what you should do) Virtual Threads Programming Model Callable<Images> fetchImages = () -> someService.readImages(); var f = Executors.newVirtualThreadPerTaskExecutor() .submit(fetchImages); System.out.println("f = " + f.get());
  41. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 51 And

    if something goes wrong: Virtual Threads Programming Model 78: Callable<Images> fetchImages = () -> someService.readImages(); 79: var f = Executors.newVirtualThreadPerTaskExecutor() 80: .submit(fetchImages); 81: System.out.println("f = " + f.get()); Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Boom! at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122) at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191) at org.paumard.MyApp.main(MyApp.java:81) Caused by: java.lang.IllegalStateException: Boom! at org.paumard.SomeService.readImages(SomeService.java:12) at org.paumard.MyApp.lambda$main$0(MyApp.java:78) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) at java.base/java.lang.VirtualThread.run(VirtualThread.java:329)
  42. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 52 When

    you need to run several task in parallel V. Threads and Structured Concurrency try (var scope = new StructuredTaskScope<>()) { } catch (InterruptedException e) { // do something with the error }
  43. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 53 When

    you need to run several task in parallel V. Threads and Structured Concurrency try (var scope = new StructuredTaskScope<>()) { var imagesSubtask = scope.fork(() -> someService.readImages()); var linksSubtask = scope.fork(() -> someService.readLinks()); } catch (InterruptedException e) { // do something with the error }
  44. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 54 When

    you need to run several task in parallel V. Threads and Structured Concurrency try (var scope = new StructuredTaskScope<>()) { var imagesSubtask = scope.fork(() -> someService.readImages()); var linksSubtask = scope.fork(() -> someService.readLinks()); scope.join(); } catch (InterruptedException e) { // do something with the error }
  45. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 55 When

    you need to run several task in parallel V. Threads and Structured Concurrency try (var scope = new StructuredTaskScope<>()) { var imagesSubtask = scope.fork(() -> someService.readImages()); var linksSubtask = scope.fork(() -> someService.readLinks()); scope.join(); var page = new Page(imagesSubtask.get(), linksSubtask.get()); // do something with page } catch (InterruptedException e) { // do something with the error }
  46. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 56 What

    if something goes wrong? V. Threads and Structured Concurrency try (var scope = new StructuredTaskScope<>()) { 62: var imagesSubtask = scope.fork(() -> someService.readImages()); var linksSubtask = scope.fork(() -> someService.readLinks()); scope.join(); if (imagesSubtask.state() == State.FAILED) { imagesSubtask.exception().printStackTrace(); } // ... }
  47. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 57 What

    if something goes wrong? (e.getCause() is null) V. Threads and Structured Concurrency java.lang.IllegalStateException: Boom! at org.myapp.SomeService.readImages(SomeService.java:12) at org.myapp.MyApp.lambda$main$0(MyApp.java:62) at java.base/java.util.concurrent. StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:893) at java.base/java.lang.VirtualThread.run(VirtualThread.java:329)
  48. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 58 So:

    - Blocking a platform thread is wrong - Fixed: Blocking a virtual thread is OK - Having a non-relevant stack trace is annoying - Fixed: Your stack trace takes you where your call is - Having loose threads is annoying - Fixed: No more loose threads thanks to AutoCloseable Concurrency Issues
  49. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 59 -

    Are as efficient - Provide a simple programming model - It’s your good old imperative model - Make debugging possible and easy - Make profiling possible Virtual Threads…
  50. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 60 Scoped

    Values fix the issues with ThreadLocal variables ScopedValues
  51. 2/5/2025 Copyright © 2023, Oracle and/or its affiliates 61 So

    many things… - They are mutable - The VM cannot optimize them - They may be kept alive forever Virtual Threads support ThreadLocal variables! What is Wrong with ThreadLocal?
  52. 2/5/2025 Copyright © 2023, Oracle and/or its affiliates 62 ScopedValues

    are non-modifiable They are not bound to a particular thread They are bound to a single method call Welcome to ScopedValue! ScopedValue<String> key = new ScopedValue.newInstance();
  53. 2/5/2025 Copyright © 2023, Oracle and/or its affiliates 63 ScopedValues

    are non-modifiable They are not bound to a particular thread They are bound to a single method call Welcome to ScopedValue! ScopedValue<String> key = new ScopedValue.newInstance(); ScopedValue.where(key, "KEY_1") .run(() -> doSomethingSmart()));
  54. 2/5/2025 Copyright © 2023, Oracle and/or its affiliates 64 ScopedValues

    are non-modifiable They are not bound to a particular thread They are bound to a single method call Welcome to ScopedValue! ScopedValue<String> key = ScopedValue.newInstance(); ScopedValue.where(key, "KEY_1") .run(() -> doSomethingSmart())); ScopedValue.where(key, "KEY_2") .run(() -> doSomethingSmart()) .run(() -> soSomethingSmarter());
  55. 2/5/2025 Copyright © 2023, Oracle and/or its affiliates 65 Welcome

    to ScopedValue! ScopedValues are non-modifiable They are not bound to a particular thread They are bound to a single method call void doSomethingSmart() { if (key.isBound()) { String value = key.get(); ... } else { throw new IllegalStateException("Key is not bound"); } }
  56. 2/5/2025 Copyright © 2023, Oracle and/or its affiliates 66 Scoped

    Values are not transmitted to threads reason: you don’t want loose scoped values They are transmitted to StructuredTaskScope reason: you can’t have loose scoped values ScopedValue
  57. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 67 Virtual

    Threads and Structured Concurrency fix the main problems of Asynchronous / Reactive Programming Without giving up on performance Scope Values fix the issues with ThreadLocal variables In a Nutshell
  58. 2/5/2025 Copyright © 2024, Oracle and/or its affiliates 68 JDK

    23 JDK 24 Virtual Threads Final in 21 Final in 21 Structured Concurrency Preview ??? Scoped Values Preview ??? Loom: Current Plan for the JDK 23