Concurrency crossroads: Choosing between reactive programming and virtual threads
About this talk
This talk explores concurrency in Java, particularly focusing on Project Loom's virtual threads as a modern approach to improve performance and efficiency. The speaker discusses the limitations of traditional blocking IO and reactive programming, illustrating how virtual threads can allow for a more streamlined request handling process. He delves into structured concurrency as a means to manage tasks and avoid thread leaks, emphasizing the ease of reasoning about code while utilizing virtual threads. Throughout the session, live demos are presented to demonstrate how these concepts can be applied using the Quarkus framework, showcasing the integration of modern Java practices in cloud-native applications.
Full transcript
Um today we're going to talk about uh concurrency and especially concurrency in Java. Uh in the past we always used reactive programming and now we have virtual threats with project loom. So who heard about project loom before that's good then you came to the right place. For the people that don't know yet we'll cover a whole bunch. So what you get from this session uh this is
the last session of the day. So, thanks for staying here and bearing with me. Uh, we're going to discuss why blocking is the ba baseline and why it hurts. Uh, what is reactive? What does it solve and what it cost you? Then we're going to dive into virtual threats and then I'm going to give you some honest trade-offs. Then we're going to drive dive into structured concurrency
if we have a bit of time at the end. And I'll show you at the end a basic matrix to to go when you get home that you can decide what you want to do. It's not a silver bullet, but it's just guidelines on what you should choose. That's me. I'm an engineer at Lunat. We're based in Rotterdam, the Netherlands. So, it was just an hour by
train uh this morning. And I've been using Quarkus since well, basically day one. Uh before that, I was mainly uh in the Scala world. For people that don't know, that's a JVM language as well. mainly focusing on functional programming, reactive programming with stuff like Aka and actors and also the plane framework. Uh nowadays I'm more focusing on building cloudnative apps, highly scalable uh with modern Java and
Quarkus. So that's why we're here today. Um I'm not going to use a traditional Jakarta e stack. Uh but we'll be using Quarkus for this demos. Uh but the same would apply to your traditional Jakarta e stack as well. Quarkus as well uh is a cloudnative framework where you can build like microservices but also full-fledged modular monoliths and it's based on Jakarta e standards and also micro
profile standards. So the the agenda for today well we already did the introduction. We're going to cover what is blocking IO then dive into reactive programming show what virtual threats are. Then we're going to do some live coding and a live demo. So let's hope the demo gods are with us and it doesn't break. Then we dive into the comparences comparisons and then we'll arrive at conclusions.
So who has used green threads in the past? If there's anyone they probably don't want to tell because that's pretty far away that will probably tell people's age. Uh that's a thing that we had in Java 101. Uh but they quickly abandoned it and replaced it with platform threads. And that's basically what I've been been using the last 20 to 30 years. Later on in JDK 5,
we got the concurrency package with the executor service and all that. Then in JDK7, we got a fork joining pool and that was for really high performance uh for doing CPU intensive workloads. Then they came up with reactive streams in RX Java to do reactive programming and later Cotlang came along along along the block with co- routines but we'll skip those for today. Then something more modern
from the last five years. We have now virtual threads. We have scope values and it's still a preview structured concurrency but we'll still show you what's coming and uh how they improved it over the years. So this is a pretty old image. It's from the Solaris documentation from Oracle. And this depicts basically how an old school Java application would use green threads in the many to one
thread model. So we got a bunch of green threads in your Java application and it would map to one single uh platform thread. So it's a bit like forman's concurrency because in the end it doesn't really run concurrent on the operating system. Then we got the platform threads. And a platform thread is basically a onetoone model. You have one in your Java application that maps directly to
to one on your OS. And then the last one, the many to many where we get a whole bunch of threads, virtual threads that can map to a whole bunch of OS threads. So keep that in mind all those old images because they're still hold through uh true in this day and age. So when we talk about blocking everyone always tells you tells you it's bad but
if you do pure functional programming you don't have input output because you can't do side effects. If you want to have a program that does truly something useful you want to have some input and output. The basics can be reading or writing to a file or to a terminal window. But in your traditional uh typical uh Jakarta e stack, you would call a remote service, you would
interact with a database, send message to a message cue. And nowadays, of course, I had to incorporate some AI, you interact with an LLM. So why is it bad? With blocking IO, you ask for something and then you do nothing. And you can already imagine that doesn't really scale well because you don't do nothing. Your your OS thread is just waiting there for your message cue to
respond with a message. Maybe you have a super long database query or your external REST API is super slow and you have a platform thread doing their It's not all bad because typically you would have a thread pool one specifically for for your database calls maybe a thread pool for your HTTP client. So they can share those resources and then well your users can actually get something
useful. So as I mentioned the platform thread maps to an OS thread. They roughly need one megaby of stack. Depends a bit on your platform can up be up to two megabytes. They're relatively costly and slow to create because the OS and the kernel have to boot up a new thread allocate some memory. But the nice thing is the OS takes care of the scheduling. So your
Java application does doesn't have to or the JVM doesn't have to. And as I mentioned, we often use fixed size thread pools for those different things. However, if I now deploy on my MacBook, it's a fairly modern one, I can spawn quite a bit of resources. But if I run on the cloud on a small machine, I might only have two or or four CPU cores available.
Especially if you run on a Docker container, there's not a lot of cores available for you. So what you typically do, you do some performance tuning. Works great on your local machine. Then you have to go to some staging environment that looks like production where you have to do tuning and tuning again to reallocate those thread pools. If you then go to a bigger uh machine or
a bigger docker container, you have to do the re tuning again and again because based on your workloads that could look differently. So for a typical request that comes into your application, it gets handled by platform thread A. It calls some remote service and then waits and does nothing. It's just there idling doing nothing. As I mentioned, it's not all bad because we have a whole bunch
of them. This example just two, but typically you have 20 or 200 threads that can handle your requests. So when request two comes in, platform thread B can handle it and also waits If we would now have request uh number three in the green errors coming in in the middle there, you get a timeout as a user because well there's no thread available to handle the workload
or the request. So well the user waits there for a bit gets time out and he doesn't get a nice failure response. So we've been using this for 20 to 30 years with all the the thread pool tuning has been performing pretty well for us so far. So I prepared a little demo. So let's dive into the code a bit. Um let's see. Is that big enough
for you guys? All right. So, imagine we have a bartender. In this case, I made it with coffees, but seeing the time, I'd rather do the demo next time with a beer around this time. Uh, so we have a a blocking beverage, uh, a blocking bartender that produces blocking beverages, and it needs to warm up the coffee machine. And imagine that your bartender presses the button on
the coffee machine and does nothing. It's not going to help any other clients in the room because there's just one bartender and just going to do nothing. Well, the other customers are going to be pretty annoyed because they it could take other people's order, etc. But no, I'm just going to sit there and sleep. Of course, don't put a threat sleep in production code, but it this
is just a demo. Um if we then have a look at the resource which is a simple J res uh resource in quarkus um we can just get it then we save the beverages that we serve to a database and then we give back the the beverage to the consumer. So that's pretty simple. We then execute that request. It's going to take a bit of time. As
you see here, uh, it's warming up the coffee machine, takes a bit of time, and it's then going to persist the coffee machine. The colors are not great on this projector, but, uh, you get the idea. I think this is a bit better. So, it takes a bit of time and then persisted. Well, if you just do one, that works fine. We don't fan out. Uh, but
in this case, we only have one. So, it's a bit problematic. So if we want to do multiple uh calls, we can just do that sequential, right? Imagine that we have three customers. They all ask for a coffee and the bartender is going to do just this in sequence. So it's going to warm up the coffee machine, wait for the coffee, pour the coffee, the machine turns
off, then the next customer, next customer, and so on. So you can visualize that as well. Now we see the the warming up of the coffee machines and then the end it returns back. This is what you typically do, right? So you fetch something from a service, you do something with it and you persist it. We can also do this in parallel. Uh for that we're going
to need something else. So we can inject a managed executors to get an executor service. And if you have your Jakarta e app, you can just also annotate it in a sim similar fashion. And then you get a executor service for What it then does uh well we can just submit them to the executor and then get those futures uh and uh return the response. So if
we execute that example, you see it's warming up the coffee. Now we have three coffee machines in parallel. It turns on all the different coffee machines, wait for all the coffees to finish and gives the three customers their three coffees. So that works pretty neat and this is what we're all used to, right? So we can either do sequential and parallel, but this is the paradigms we've
been using for the last 20 years and it works fine. However, under high load, I've run this super fast with a thousand connection uh requests at the same time. For example, we will only have 20 worker threads by default. the rest of my customers would just get a timeout or get rejected and that's bad. So what did people typically do 10 years ago to to fix this?
Everyone said let's go reactive. Reactive was going to fix everything. Every was everything was going to be fast and we fix all the problems with blocking. they came up with the reactive manifesto and uh at the po the bottom or at the top we have the responsive so we always want to give response back to our consumers so they always get a reply so what's at the
core of this things should be messaged uh resilience so they must adapt to fluctuating load uh that's the elastic story and the resilient is that you handle uh failures gracefully so you give a nice error message back right away and you just don't let the consumers just time So why did well what what are reactive systems all about? They're are non-blocking. So we don't block the world
or a threat but we can continue doing something useful. Uh this way we can handle CPU and memory resources a lot more efficiently. And a difference as well that with a single thread now can handle multiple requests. So instead of instead of sitting there idle, it can actually do some other work in the meantime and handle requests for other users. So in this case, you only need
a handful of those uh threads and we call them event loop threads. Going to see a bit more details later on that as well. And typically the event loop threads will match the number of cores in your machine. So when it boots up detects well your machine has four CPUs or eight virtual cores and it going to spawn eight event threads. Now we also don't need to
pull them because well we just have this uh uh event loop thread that can handle So imagine a single core system uh for example a Docker container that you really squee squeeze squeeze together that just get one virtual CPU and this event loop thread uh can handle multiple requests at the same time. So it's in the end still backed by a platform thread and when request one
comes in it calls a remote service it can actually do some meaningful work in the uh in the meantime. So when request two comes in, you can just handle that, process that, and so on and so on. So there's quite a difference from this image where we already needed multiple threads to handle multiple requests. In this case, we can just do one thread. We can handle multiple
The nice thing is in Quarkus, you don't have to choose in your application. You don't have to go full reactive or full blocking. we can do both at the same time. So how can it do that? Um if you may be a bit used to JavaScript for example, it looks fairly similar. Uh we have some event Q where the request comes in. Then we have the EO
threads uh or the event loop threads pulling for events. So request comes in, it's going to find the request handler and dispatch it there. So most time if we can do that non-blocking it can just handle that event and do something. If you now if Quarkus now detects that something will be blocking it can switch this to a worker thread and delegate this still to uh a
thread pool. So it can handle that one blocking. So if you see them next to each other uh on the one side uh with the pink we have the the blocking worker thread. So we have a pool in this case just two uh where multiple requests comes in that get handled but they can't do anything else. In the meantime we have our event loop thread that can
just handle multiple requests interled at the same time and can do many useful things in between. So it doesn't have to wait and stop the world. Now this is a bit cumbersome to program with. So lots of people came up with clever ways to make it easier for you to program in those par paradigms. If you use Quarkas, you have lots of options. Uh you can use
mutiny with a uni and a multi that looks a bit like a mono and a flux from spring. You can use your good old completable future. Uh or you can use your reactive stream publisher with RX Java a streams but also mutiny. And if you're in the cotlin world, you can use your cotling go routines as well. So then we need two kind of paradigms. We have
a uni on one hand and that can emit a single event. So it emits one item. So maybe one response from your external service or either a failure. It's one of the two. It's something or an exception. In this case the exception is a failure. Then we have a multi and that's more for streaming where you can have multiple events and multiple can mean zero or any
and again as I mentioned that's similar to a mono and flux from spring if you've seen that before. Then we have some useful uh methods on there uh to chain those. So here I statically just initialize a uni from one single item and then I can default uh define my pipeline of operations uh on what should happen with that item. So I say when when I get
that item with on item I can transform it to a string and append this number then I can expand my uh chain. I say on this item delay it by uh 100 milliseconds and in the end I need to subscribe to it because in the first few lines are just your pipeline definition. If nothing subscribes to it nothing will happen. So you really need to subscribe and
then define how you handle that request. On the other hand, you have a multi at the bottom where you can create it from a whole bunch of items and you can transform them. Uh you get the first ones. Uh you can also recover if you get an exception with a default item and again you need to subscribe to this pipeline. For people that are not so familiar
with this paradigms, it's a lot to learn from those libraries. So it doesn't really matter if you use mutiny uh completable future or or anything else. Your you and your team really need to learn all those paradigms and all those methods from your library. Uh for people that you're a bit more used to functional programming that would call this on item transform a map um or the
on item or the select first filter for example or take first. So there are multiple ways how we can deal and the nice thing is in quarkus most of the extensions just support doing it the reactive way as well. So you can see the rest layer the rest client uh we have hibernite reactive for jpa we have reactive messaging uh and also that serverside templating is also
reactive. If you want to know more, uh, check out all the extensions because there's a lot. But you can in a caucus application just mix and match and choose what you So let's see those in action. Um, I have my reactive bartender in this case. Let's make that a bit more readable for the you guys. And here I define my pipeline. So I create something uh for
muni and I say I delay it by some delay that I configured. Then how can we use that? Let's minimize this where we say get this thing and if I get this item I want to persist it and then return a rest response. So you could also probably uh write that with a map for example. But in this case uh we're doing on item call we get
this one it does the same of course but take the notion this is now happening from the vertex event loop. So it's the event loop threads that handle this request and well if multiple requests come in in the meantime this vertx event loop thread can handle multiple requests in between for you but up to now it still looks pretty much the same. If we then get to
more complicated examples and especially in this view, it's not really readable. But if you've done reactive programming before, you tend to get in those nasty call chains. And this is one that's still relatively simple simple because we just do three maps inside each other. But imagine getting your newly junior member on the team spawning all this kind of code in his face. uh it's going to have
a trouble uh reading this, understanding this, and also changing this. We can see what happens. Uh if we do the again, it's just going to do them one by one by one. Well, it's more complicated code, but at least you're not blocking We can also do this in parallel, of course. And for this um let's see if we want to do this in parallel we're just going
to define the separate unis all the different ways to get something and then we can have a join strategy. So we could do a uni dojo join and then we can also give it a strategy on failure. So we can say get me all the things in and collect the failures as well. What we could also do uh let's see fail fast. So imagine one of the
the the the units above fails and say fail fast then it cancels out the other ones and just returns you the error. Depends a bit on the use case which you want. Sometimes you want to try two or three different services internally to get your data and get the first one that comes back. uh you want all of them. So if we have a look there uh
in you can now see uh let's highlight that that they're all coming from event loop 3. So the event loop 3 is actually handling all those different things at the same time. If we were to look at the the worker threads, it will be a unique worker thread for each case that will be spawned. But in this case, it's just one event loop thread that can handle
all those parallel calls in one go. And why was it again? It was because of this. It could just interle them all, schedule them all, and when it gets back, it joins them and produce the result. And you can imagine if you now get a message skew in there, a database in there, and you have this nasty uh interleafd uh reactive code, it's really hard to start
debugging this. Something fails, you put a debugger in, it jumps around all the place, stack traces are not readable, exception traces are not readable, etc. So well you get a lot of performance but it will be a lot more difficult to reason about and also some gotchas that we saw in production as well with code that perfectly compiled especially transaction handling is a bit more difficult. You
really need to think how you handle your transactions and don't start doing stuff in parallel with the database uh but really do that in a proper way. So that makes it really yeah kind of hard to deal with this kind of code. So what did they come up with? They looked at other programming languages and they came up with project loom with virtual threads or also they
called it fibers in the past. Uh virtual threads are cheap to create, don't lose a don't use up a lot of memory. Uh they're managed again by the JVM. So going back to the green threads that we saw before and they're cheap to block. They're meant to be blocked and you write your good old imperative code again. However, this is only going to be useful when you
have IO bone tasks. So a lot of people think, well, I got my virtual threads looks super great. I'm just going to enable virtual threads all over the place in my application and it's magically going to be faster. Well, that's probably not going to be the case. This mean makes it easier to handle uh IO bound tasks and we're going to see that a bit later as
well. So how does the JVM do it under the hood? It uses something called the continuation and that a continuation uh represents something that's pausible. So plausible unit of work. So when it does detects this when when it's waiting for the uh it captures the execution state. So the stack and the local variables so it can be reused later or resumed later. So when when uh a
virtual thread starts doing some work is mounted to a platform thread and this time we call it a carrier thread. So basically a platform thread carries along a virtual thread and then does some work and then it can yield or park uh because on blocking IO the continuation yields and then this call stack or partially uh stack is then moved to the heap and then freeing the
carrier thread so the platform thread to do some other work. So this is way platform threads can have uh do multiple things. So they can carry on a virtual threat do some work uh yield it and then grab another one. Once that IO completes uh you can unpark it or resume it and then it submits the continuation again and continues on. So the nice thing here is
it copies back everything that was on the from the heap back to your local stack. So if you then get a ST trace, you have the full stack trace again. You don't have those weird stack traces that you get with reactive programming. So how does it look in term of uh the previous diagrams? It's basically an overlap of the previous two images that we saw before. So
now the pink request request one one coming on uh and maps to a new virtual thread virtual thread one. So for every request that comes in, we're going to create a new virtual thread. We should never pull virtual threads like we did with platform threads. For every operation that you do, for example, a new request, you spawn a new virtual threat. Well, this virtual thread needs to
do some work. Maybe needs to process some JSON, for example. Uh so the the platform thread carries it onto itself, does some work, and then parks the continuation while it's waiting for the remote service. The nice thing is the virtual thread one itself is blocked but it doesn't block the carrier thread. So this carrier thread can pick another virtual thread do some minial work and park it
again. So in this way we still have well one underlying OS thread but we can reason as if it was multiple threads. So every request again gets their own thread. Well, you think that's great. Everything is fixed. I again move everything to virtual threads. My life is done. My application will be super fast. Well, when they released it back in uh a couple of years ago with
Java 21, we got some gotchas because some threads will be pinned, mainly uh JDBC drivers that used uh synchronized monitor blocks under the hood. Uh that would cause thread pinning. We're going to see in a bit what it actually does during that pinning. The good news is however uh everyone worked really hard to solve those issues. So most of those libraries that were using those synchronized blocks
have been fixed by using for example uh with a Java util concurrent lock uh workaround. So for example re-entrant lock you can replace those synchronized blocks and get rid of the the pinning. At the same time they improved the JVM as well. So uh in J JDK 24 they fixed this issue entirely together. So while a lot of libraries were already fixed the JVM also fixed it.
So for most cases you wouldn't see pinning. This doesn't hold for all cases. Of course if you go into the world of native calls with JNI or the foreign uh memory API then you could sometimes have some pinning as well. If you read the chat proposals that release some specific cases where you do native calls, then you still need to watch out for uh pin but it's
explicitly mentioned and you probably won't see it that often either, but be aware that can still happen. So what is this pinning? So we have the again request one coming in on virtual thread one. So we spawn at virtual thread one. Uh the platform thread uh mounts it and does something useful. However, to the due to the synchronize block, it can't unmount it. So there is some
native memory still mapped somewhere. So it can't take it off as thread and put it somewhere on the memory. But yeah, it really has a lock on there. So it can't move it away. So now we're in a bad situation again because we think, well, we we're great now. We're using virtual threads, but we still have an issue uh that this carrier thread is now locked and
can't do anything meaningful. Again, as I mentioned before, it's fixed in JDK 24 with J 491 and you can read the edge cases where it doesn't work, but they're working on improving those situations as well. So, how would you detect that? Because typically that doesn't pop up uh during development work or during test work. will pop up in production. So that's where you could use JFR events.
For example, uh with this JDK virtual thread pin event, the JVM will evit those event emit those events. So you can monitor those JFR events to see if you have some impact. If you then encounter any of this, well, probably need to go and fix some lying library. or if you're doing for example uh native calls you can wrap them with a re-entrant lock instead of this
those synchronized monitors. So the good news is most of it's fixed and otherwise if you're impacted it you probably can fix it yourself in your own Then another thing why it doesn't magically make your code faster. Uh we can get monopolization for virtual threads with the carrier threads. So uh this happens where you have CPUbound workloads without doing any uh IO. If you have those cases, you're
probably better off to hand off this work to a dedicated thread pool again that you tuned and configured before because virtual threads are probably going to make it worse for you in this So imagine again that we have one platform thread, so one carrier thread. It grabs virtual thread one, but it's doing so much CPU work, maybe doing some heavy calculations that it can't park it somewhere
because it needs to do CPU work. So this virtual thread 2, if it wants to do something, it's going to need another platform thread. Well, you can imagine there's a slight overhead in all this. So that if you uh have really CPU intensive workloads delegated to a dedicated thread pool that you tune uh properly as well. The nice thing is with virtual threads because they're pretty new
but most frameworks nowadays support them. Uh Quarkus brings you some goodies as well. So by default because a thread a normal thread would have a name on the JVM but a virtual thread doesn't have a name. It's a bit the same like what what they say for Kubernetes uh pets versus kettle. If I have long live threats I give them a name. If I have shortlived threads
why bother giving them a name because they just do their task and then just they go go away again. So I don't really give them a name. in quirkus by default in dev mode you do get the naming uh so it's a bit easier to debug and reason about your code they also provided the JUnit extension to to detect the pinning so you don't have to wait
till production you can actually catch it during integration testing for example uh you could with those annotations right before we go into structure concurrency let's uh show the virtual So in this case we have our virtual bartender again and it looks the same as our block beender uh bartender because it just does a thread sleep. In this case it's not bad because we're going to run this
on a virtual thread. The only thing we need to do is annotate this with run on virtual thread. If you're more in your classical Jakarta e platforms, you can get a manage executor service and annotate and get a virtual thread in there as well. A bit of similar as you see here where you can get it in this fashion. So in this case we can just call
uh a bartender get and we can safely block the world because in the meantime it could do something else. So let's see that in action. And you can see now this happens on virtual thread zero and the entire uh stack is handled by this single virtual thread. So we have a single virtual thread that's handling the work. If we do that again, we're not going to reuse
it. So normally for your worker pool you will get the same thread back again because it will be released from the pool and can do something again. But in this case the virtual thread is just a one-off use. You now get quirkus virtual thread number one and so on and so on. So for every request that you do you have again your nice model for for uh
a threat per request again. So it's easy and nice to reason about it easier to debug and step through. If we then have a look how to do that in sequential. So sequential is just as easy as it was before the blocking way. Then have a look at the logs. If we do sequential just virtual thread number two doing all those works. Well, that's not really useful
because we now a virtual threat. So, we could just spawn a virtual thread of unit of action that we want to do. So, if we expand a bit on the example, we could also do this in parallel. So, we have this executor that we submit and then we get all the results in one go. If we have a look in there, what will happen then? So, we
do them in So if you see at the top the request that came in was handled by Quarkus virtual thread number three and because we then use an executor service to submit the subtasks to new virtual thread. So every subtask that we now submit is going to get its own virtual thread. So you can see virtual thread four, five and six. Uh so those will be children
of the the parent virtual thread and then in the end they will be joined and the response gets back on virtual threat three where it started. So again it's a a threat per request model but we can also fan out uh and get child virtual threats that can do some meaningful work. Let's see. You can have uh custom uh thread executor services as well. Uh but let's
skip that for now. So now we have virtual threads and we're almost there. So it looks pretty nice. Uh but we don't have all the goodies yet. So for the next goodies, we would also need structure concurrency to to really make it easier to write highly concurrent code again in Java. Uh this is to avoid threat leaks because if we would have in my old situation in
the blocking world, uh if I would do the situation where I had fan out to multiple other threats and one of them fails, the other threats that have spawned would just continue and be lost forever. uh and then in the end of uh the world you get out of memory exception because well there's so many abandoned threats somewhere that you need to restart your It's also uh
we want to go back to those cancellation strategies that we saw with reactive programming. Do we want you want to fail fast? Do we want to collect all the failures? We don't have that really yet with virtual threads. So for that we need structured concurrency. Um it's still in preview. So in Java 25 it was the fifth preview. Now I have Java 26. It was in the
sixth preview and they plan to do a seventh preview in JDK27. So when will we get this? We don't know uh because they keep on iterating and improving uh the API for this. But we can have a brief look uh at how it looks. Again, we have our blocking beer tender, but it's now called a structured bartender, but it's just exactly the same. Just a threat sleep.
And let's close this one. What are we going to do now? Uh, we're going to open a structured task scope. So, we say structured task code open. And this is an example what changed in all those previews. So we used have to do uh new structured task scope something something something but it provided some uh static factory methods to make it easier for you to write this.
Note that we also have to do it in a try with resources block so that JVM can clean up the the the scopes that is spawned as well. So when we now get in here, we're going to fork this scope. And the nice thing is uh they also release scope values and then put in the presentation. Uh but it's basically an alter alternative for your thread locals.
Thread locals could leak all over the place. Uh everyone could write to them and read to them and who knows what the status was. with scope values. When you open such a structured task scope, those scope values would only be scoped to the subtask in there. And they couldn't change the outer scope. But for now, we keep it simple. We don't use scope values in here, but
we're just going to fork the scope uh to create our uh child threats again. And at the end, we should call join. So we spawn all the child threats, let them do something, we join them and now we can safely get because uh the if there was a failure the join would throw an exception and would never get to the get. So the get is now a
safe call and then we can just obtain all the beverages. Well, now we can also do custom things in here. It's a bit hard to read on the zoomed So we can first define a custom joiner where we say all successful or throw. But here we can also change the strategy all until any successful. So we just get one and disregard the other results and terminate those
virtual threats. Uh await all etc. The nice thing is if you have highly uh concurrent code uh in your own application, you could write your own joiner. So based on your own business needs or business case, you can also define your own joiner in here. We're not going to show that right now. Uh we can give it a thread factory. So by default, as I mentioned before,
virtual threads don't have a name. So in this case we grab the the name of the the outer virtual thread and then generate the names ourselves. So they have a meaningful name again and then we can fork and the nice thing is uh those are all subtasks but here I had to assign them to a variable. So beverage one, two, three, all all two variables. But here
I can just do scope.join. And I just get my list of structured beverages in one go. And this is also one of the improvements that was made to this API in preview features. In previous versions, you couldn't do this in one go. But because we give the joiner some information about what it should return from its subtasks, we can just do scope.join join and we get the
list of the things that we asked for. So that makes it a lot nicer to work with. We then have a look uh how that works. So let's see. So again we get it on the quarkus virtual thread 7 and we're going to open a structured tcope uh spin someh spawn some subtask. So we gave them a useful name because otherwise it wouldn't have a name. If
you for example uh do the normal one now let's see simple you see here they get started on the outer virtual thread but the spawn children don't have a name anymore by default. So that's why if we do it the custom way they do get a nice name again. So that's a bit what you can do with the structured task uh u API where you can really
define everything how you want See that's then the full example here where you can get them directly. So that's even a bit nicer than assigning everything. and then getting them all of them. But here you can just because you you know you gave this custom joiner you can just do scope join and you get them all in one go. So this is looks quite nice but they're
looking for active feedback from the community as well. That's why they keep on doing this preview features. So I would say try it out but it's hidden behind the preview flag on the JVM. So don't use it in production. It's fine to mess around uh on your local development or a test environment, but don't put this stuff in All right, we're almost running out of time, I
think. No, we got a bit of time. Sorry. Oh, then if you have one or two minutes, then uh we're gonna go through the comparisons and the conclusions and then we're done for today. So on one hand we had blocking in the old world with imperative programming with a shared worker pool things executed sequentially and they were easy to reason about at the cost of higher memory
usage limited amount of threads and limited amount of throughput. Then we had reactive while they were harder to write or debug with the callbacks and reactive code you could handle multiple requests on the same uh thread. you could achieve higher throughput while at the same time having lower latency and lower memory usage. Then in the new world we have virtual threats with imperative programming where you have
again a threat per request. So it's nice and easy to reason about to debug uh to investigate uh slightly lower memory usage. However, if you want to do the more complicated uh concurrency things, you don't have structured concurrency yet uh available uh for production use cases. So, what should you do when you get home next week? Uh try to enable virtual threads in your application. See what
happens. But I have some guidelines for that. So, if you have your legacy apps or a simple crut service with low concurrency needs, why bother? But at the same time, you could try it out. Have some fun with As pick reactive or stay on reactive when you need the full performance, the control and the high fan out for use cases where you have optimized for P99 latency
and throughput. And that's mostly ideal for like gateways and streaming use cases. Pick virtual threads for imperative code at scale because they're easy to reason about, but test them first first and watch out for the pinning gotchas. And again, use the structured concurrency once it final. And the nice thing what I like personally in Quarkus, you don't have to do one or the other. If I use
Spring Boot, I enable virtual threads globally and everything is virtual threads. in your Quarkus application you could just have a modular monolith and do all those three or four things in one place. Well, you can find some of the resource there. Find the slides and code online and uh thank you for today. If you want, there's some time for questions. Otherwise, you can find me at the
drinks as well. Or if you have any war stories to share later over a beer or a drink, that would be fun as well. Right. Thanks,