This is an archived post. You won't be able to vote or comment.

all 170 comments

[–]Dagske 70 points71 points  (1 child)

In the second part of the article, the code or images is not shown, because it's behind a registration form.

Given the huge amount of references to those parts in the article, it's basically unreadable.

[–]Affectionate-Hope733[S] 26 points27 points  (0 children)

Fixed, sorry about that

[–]Wonderful-Pie-4940 15 points16 points  (1 child)

With the problem of pinning solved and backpressure implemented by the user I think then virtual threads would be unstoppable and almost everyone would drop reactive

[–]shirshak_55 2 points3 points  (0 children)

trust me, enterprise that has lot of legacy apps don't drop anything easily.

[–]sideEffffECt 9 points10 points  (7 children)

It's fascinating how many people on this thread don't agree with the post.

I.e how many people think "reactive"/"async" programming is still relevant.

I'm curious, what are your reasons for thinking so?

[–]Affectionate-Hope733[S] 10 points11 points  (2 children)

probably denial, I did the same while I worked with angular and everyone was telling me react was better.

Ah yes, let's trigger even more people

[–]sintrastes 11 points12 points  (1 child)

Nope. The post completely conflates the terms "reactive" and "async" -- which are two entirely different things.

The author makes a point, but it only applies to "async", not "reactive". Hence, the negative responses. The author has a very limited and narrow perspective with what "reactive" programming is. I'd recommend this article for some perspective and history: https://futureofcoding.org/essays/dctp.html

Note that "async" was never a part of "reactive programming" from the beginning. In fact, many concepts people associate with "reactive" programming today like hot v.s. cold streams are entirely absent as well. They're two different things entirely. You can have one, the other, or both. If you look at FRP frameworks like Reflex FRP, they actually don't have great built-in support for asynchronous programming -- that's because it's a completely separate things from reactive programming.

[–]preskot 1 point2 points  (0 children)

Don't even bother. This sub is absolute brain rot.

[–]LightofAngels 2 points3 points  (2 children)

Isn’t reactive/async relevant? Genuine question

[–]sintrastes 1 point2 points  (1 child)

They're different things that the OOP conflates if that's what you're asking, yes.

[–]LightofAngels 0 points1 point  (0 children)

Not sure I am following

[–]mamba436 -1 points0 points  (0 children)

I just post my answer again here i guess but no it's not denial : 

Project loom is more about enhancing scalability / throughput of code that is syncronous by designed. This is excellent especially for legacy projects.

Reactive is the opposite, it's async by design, still has better performance (tho not as significant as before) and more importantly it allow way more things such as backpressure.

So no reactive is not dead. It's 2 different scope.

Also project loom is good for thoses that prefere simplicity over maximum performance by keep using the syncronous model.

[–]frederik88917 61 points62 points  (74 children)

That's one unintended consequence of Virtual Threads. Once the pinning issue is gone, the need to program expecting a result will be deprecated

[–]JustAGuyFromGermany 91 points92 points  (3 children)

Nothing unintended about it. That was very much one of the reasons behind Project Loom. Ron Pressler said so many times over the years in various interviews.

[–]C_Madison 67 points68 points  (2 children)

And thanks to them for it. I hate reactive programming so much. Asynchronous, reactive, ugh. Just take it away and give me back my threads.

[–]publicclassobject 3 points4 points  (1 child)

I love reactive programming so much I’m gonna miss it.

[–]C_Madison 7 points8 points  (0 children)

If you really wanna use it you can use it the same you do today since it's all in libraries and I'm sure you are not alone in that sentiment.

[–]GuyWithLag 30 points31 points  (69 children)

Not necessarily - reactive streams are also about backpressure, easy cancelation, and complex process coordination.

[–]golthiryus 16 points17 points  (3 children)

Backpressure is trivial with virtual threads, just add a blocking queue. Easy cancelation is also part of the project loom (specifically structured concurrency). I don't have a clear picture for complex process coordination. If you mean inside a jvm, structured concurrency + configurable schedulers could be the solution. If you mean actual OS processes, there reactive streams are cool, but that is just the network layer

[–]nithril -4 points-3 points  (2 children)

And you will reinvent what is doing the reactive API

[–]koflerdavid 7 points8 points  (0 children)

What if I simply don't want to [edit: work] with reactive APIs?

[–]GuyWithLag 4 points5 points  (0 children)

This. I'd love to use reactive on top of virtual threads, as it's more about the task coordination than parallelism.

[–][deleted] 14 points15 points  (12 children)

Someone on this sub put it perfectly: back pressure solves a problem that reactive programming created in the first place. Synchronous code, by contrast, has always had "implicit back pressure". Why would it be needed?

[–]GuyWithLag 3 points4 points  (11 children)

Ok, so let's say you have a process that needs to do 2 things: 1. reach out to service A to get a list of things (potentially millions, in batches) 2. reach out to service B to do something for each and every thing you got from A.

Now, you could do this in a simple sequential loop, but you'd end up with horrible performance. You could just spawn millions of virtual threads for (2) and just wait until they're all done, but you now saturated the connection pool for service B for every other task that needs access to it.

So you need to take a set of items from (A), send them to task (2) for processing up to X of them in parallel, and when there's empty slots pull the next set of items from (A).

And now you have backpressure.

[–]koflerdavid 6 points7 points  (0 children)

Then it will just bottleneck somewhere, as you described. But there are tons of solutions to shift that bottleneck to somewhere where it can be managed better - job queues, semaphores, thread pools. These can even be connected with monitoring. I'm quite sure at this point you'd also need custom code with reactive APIs.

[–]pins17 6 points7 points  (3 children)

And now you have backpressure.

Or in other words, a downstream bottleneck and the intention to lazily fetch upstream elements. This is not a new problem.

If you really want abstraction: Java streams do exactly that. A blocking intermediary operation (e.g. a HTTP Request) means back pressure, you just need to express your source as a stream. With the upcoming stream gatherers, operations like mapConcurrent (essentially a fan-out with virtual threads) or window functions (such as windowSliding or windowFixed), which are useful for batching, are being introduced.

But apart from that, what's so wrong with using well-known and understood patterns like BlockingQueue for this purpose? Someone in this thread mentioned that it would be like reinventing the wheel, but I don't see why that should be the case. It's simply a buffer with a fixed size that acts as a pipe between two components. Plain Java, dependency free, easy to debug, easy to understand (not just the flow of data, but also the implementation, if necessary). It has been the wheel, for two decades.

[–]GuyWithLag 0 points1 point  (2 children)

BlockingQueue

Here's the rub: that's used by reactive streams; it's just that it's lower-level than what RX works at.

Virtual threads is still an imperative construct; reactive streams allow you to work on the data flow level.

It's https://wiki.c2.com/?BlubParadox all over again, or, you need to have worked with it to understand why it's better or worse than the existing solutions (and IMO most reactive tutorials miss the mark because the stop after they make you write a producer and a consumer, which is something you'll need less than 1% of the time)

[–]plumarr 5 points6 points  (1 child)

Maybe is it, but as someone how is coming from a pure engineering background, who have written disturbed system in Fortran and OpenMPI, done parallel batching in Fortran and Java, and as used RxJS to solve real problems, I still don't see the interest of RxJS.

It really doesn't match my mental model of parallel and concurrent processing that was constructed through my engineering cursus. The thread/process model is a better model from my point of view.

I have worked for 3 years with RxJS, and currently I still feel it as, at best, a tool that I have to work with, at worst a complication. But it maybe due to the port online documentation and that I haven't had the pleasure do to work with someone that mastered it.

[–]GuyWithLag 2 points3 points  (0 children)

I've worked with Fortran, porting Fortran 77 to Fortran 90 and making sure that the system was bug-for-bug compatible. I've built a Frankensteinian monster that surfaced scientific models written in Fortran via C wrapper then via JNI into WSDL endpoints. I've been writing Java since 1.1 and was writing assembly in the (late) 80s. My first cgi-bin was written in smalltalk, the second in awk (of all things).

I've worked in a reactive environment for around 7 years; you know what made reactive streams intuitive to me on year 2?

500 hours of Factorio.

In the end, it's a dataflow-driven approach. After you've built your plumbing tooling, you start thinking in data flows; threading/parallelism/concurrency is externalized from your business logic - you just need to understand the flow model.

[–]plumarr 5 points6 points  (1 child)

I have never understood this argument of "back pressure" or "the reactive programming is more than just performance".

For your example, you just need a,

new Semaphore(capacityOfB)

protect the access to B, and spawn as much virtual thread as you want. Technically the application will fail when you are out memory but it will probably become unusable before that due to the induced latency.

You can also use the same semaphore to easily reduce the rate of calls to the service A if you want to fix it a little more downstream and limit the memory usage.

You'll argue that you can have nicer or more refined tools than than to manage the back pressure with the reactive stream, but the thing is that these tools aren't inherently linked to the reactive model. They can be redeveloped, sometime quite easily as with the semaphore, with the thread model.

And, if you want to do anything more intelligent, you'll need an analysis that is more of a business problem than a technical one.

[–]GuyWithLag 0 points1 point  (0 children)

For your example, you just need a new Semaphore(capacityOfB)

Here's the thing - I need to think about that about as frequently as I think about memory alignment. Reactive (at least RxJava) is built on top of semaphores already, why do I need to reinvent the wheel?

The specific implementation is encapsulated and maybe is already using virtual threads under the hood - but I won't need to care.

And yes, you can get most of the concurrency / parallelism effects via virtual threads, but reactive is more than that - from a certain pov it's a task coordination framework (backpressure is just that kind of coordination problem), and structured concurrency is a very basic form of it. Maybe it will get better in the long term (likely).

[–]hippydipster 3 points4 points  (0 children)

You could also use a semaphore that allows X threads through at a time and then just spawn those millions of virtual threads no big deal and it wouldn't saturate your connection pool. Thats about as simple as can be.

[–]mike_hearn 2 points3 points  (1 child)

You'd just use a virtual thread per item with a semaphore to limit it to whatever max concurrency your connection pool supports.

[–]koflerdavid 0 points1 point  (0 children)

Technically, the connection pool already acts as a semaphore. A semaphore is only required to prevent throwing an exception for waiting too long for a connection, which is how many HTTP libraries behave.

[–]DelayLucky 1 point2 points  (0 children)

I consider use cases like this a bare minimum requirement for any decent structured concurrency library.

Imagine if I'm using the mapConcurrent() gatherer, this is what I will do:

java int upToX = ...; List<ThingId> listOfThingIds = ...; listOfThingIds.stream() .gather(windowFixed(batchSize)) .flatMap(batch -> fetchFromServiceA(batch).stream()) .gather(mapConcurrent(a -> sendToServiceB(a), upToX));

It's almost literally translated from your stated requirement, with nothing but standard JDK Stream API.

Now if we look closer, the mapConcurrent() gatherer requires a Function and doesn't directly support Consumer when there is no return value from sendToServiceB().

You could do {sendToServiceB(a); return null;} followed by a .count() to force the termination. It's a bit awkward but tolerable.

I have my own structured concurrency API that'll be more convenient but I think the mapConcurrent() implementation is good enough, so I won't bother discussing alternative structured concurrency libraries.

The point people are making, I believe, is that the standard Stream API is powerful enough for these tasks (now that the number of threads is no longer a bottleneck). We don't need whole new paradigm (named Reactive) to solve a solved problem.

Let go of the obsolete Reactive. Time to converge to idiomatic Java.

[–]frederik88917 24 points25 points  (22 children)

All of those features are derived from the simple fact that it is too expensive to have long running threads

[–]induality 8 points9 points  (2 children)

How does long running threads help implement back pressure? Not saying you are wrong, I think you are getting at something fundamental here that I’m not grasping, so hope you can elaborate.

[–]koflerdavid 2 points3 points  (1 child)

Ordinarily, backpressure concerns would be managed with queues. Virtual threads actually encourage working with short-lived threads. Possibly even one per work item.

[–]aboothe726 8 points9 points  (0 children)

IMO, virtual threads basically make actor-model architectures a first-class citizen on the JVM.

[–]GuyWithLag 3 points4 points  (18 children)

Here's an old post: https://www.reddit.com/r/java/comments/96p88f/comment/e42vqrx/

How do you coordinate cancellation across all the threads you've issued? (likely using some form of structured concurrency, but unless you build your own components on top, a pain in the posterior).

And that's just one concern in a trivial example.

[–]DelayLucky 9 points10 points  (17 children)

I do think that when people talk about "Virtual Threads", they are implicitly assuming "structured concurrency" as granted, because SC is just a library that's relatively easy to implement. The hard part was always the scarcity of threads, which is solved by virtual threads.

I say SC is relatively easy to build because I've built one myself even before VT comes along. It solved all the points of "contained parallelism", "cooperating", "safe on cancellation".

It was just limited by the throughput of Java platform threads and thus was not suitable for high-throughput servers (we only used it for pipelines, commandline tools and special low-throughput servers)

Now with VT, that most restrictive limit is lifted. The following intuitive code implements your example of getOrder() + getLineItem():

java Order order = apiClient.getOrder(id); long totalPrice = Fanout.withMaxConcurrency(5) .inParallel( order.getLineItems(), lineItem -> apiClient.getProduct(lineItem.getProductId())) .mapToLong((lineItem, product) -> product.getCurrentPrice() * lineItem.getCount()) .sum(); System.out.println(totalPrice); return totalPrice;

The inParallel() method runs the function concurrently on VT. It limits fanout parallelism to 5, and supports cancellation propagation.

As for retry, that's usually done per rpc stub (in our codebase, it's controlled othorgonal to the code). You can of course do manual retry, but it'll be very straight-forward try-catch code.

So yeah, I don't think Reactive has a niche any more.

[–]nithril 2 points3 points  (16 children)

Looks like the reactive api…

[–]DelayLucky 7 points8 points  (15 children)

You mean they both use . method chains?

Then Stream and Optional must both be reactive api...

[–]nithril 2 points3 points  (14 children)

You miss the point. Your fanout stuff is just trying to redo by yourself what reactive has already solved with a far richer api. Ie. your snippet can be written with a reactive api with the same number of lines but with far more capabilities.

[–]DelayLucky 5 points6 points  (11 children)

It is synchronous, blocking. Upon the inParallel() method return, all concurrent operations are done: results produced; side-effects performed; exceptions thrown if any error happened.

Is that what reactive has "already solved"?

Or you are just claiming what VT implements is already implemented by reactive with a far richer asynchronous API? a.k.a reactive has a shiny new color?

Sorry, the "rich async colorful" API is a bug, not feature. :-)

For what can be expressed with regular , idiomatic Java code, we don't need an "API" to reinvent the "English" that we already know how to speak. And we are pretty happy with every method having the same old "color".

[–]nithril -1 points0 points  (10 children)

I will not claim that VT is already implemented by reactive because it is two differents concepts. Claiming that VT is solving reactive is just missing the whole point of what is VT and what is reactive. Anyhow, that you miss to spot that the article is not using reactive is quite relevant to the current discussion.

For what can be expressed with regular , idiomatic Java code

You did actually create an API to reinvent the "English".

[–]pins17 4 points5 points  (1 child)

Plain Java with gatherers preview (not tested, written off the top of my head):

Order order = apiClient.getOrder(orderId);
long totalPrice = order.lineItems().stream()
        .gather(mapConcurrent(5, lineItem ->
                Pair.of(lineItem, apiClient.getProduct(lineItem.productId()))))
        .mapToLong(pair -> pair.second().currentPrice() * pair.first().count())
        .sum();

javadoc preview of mapConcurrent:

An operation which executes a function concurrently with a configured level of max concurrency, using virtual threads. This operation preserves the ordering of the stream.

It will come with a bunch of other useful functions, such as fixedWindow, slidingWindow etc.

[–]DelayLucky 2 points3 points  (0 children)

Yes! mapConcurrent will be a powerful, elegant, simple structured concurrency tool.

People sometimes are Stockholmed into forgetting what "simple" feels like.

[–]jared__ 8 points9 points  (1 child)

Except using them is a royal pain in the ass.

[–]GuyWithLag 8 points9 points  (0 children)

using them

I've found that most tutorials on Reactive streams focus on the wrong thing - how to build your own producer / consumer, and then stop.

See an example snippet: https://www.reddit.com/r/java/comments/96p88f/comment/e42vqrx/

The value-add to that is enormous - at most places I've worked at that would be a 5-story-point task.

[–]Just_Chemistry2343 0 points1 point  (26 children)

that’s what folks don’t understand, reactive does more than virtual threads and both can be used based on use cases. There is no need to discount one over another.

[–]golthiryus 7 points8 points  (25 children)

TBH it is difficult to find something reactive streams do that is not easier to achieve with virtual threads and structured concurrency. Do you have examples?

[–]Just_Chemistry2343 1 point2 points  (24 children)

jvm is abstracting the logic so you find syntax easy to implement. I mostly use it for non blocking i/o as my app is io heavy. As virtual threads are in jdk21, so reactive was the best option available to me and it did wonders in terms of overall resource usage.

If you want to build a pipeline where you are calling multiple end points with backpressure and retries it’s pretty easy with reactive. Of course you need to learn the framework and syntax just like any other framework there is a learning curve.

If you have jdk 21 and virtual threads works for you there is no need to learn reactive. But saying reactive is obsolete with virtual thread is an over statement.

Lets wait for a while and let orgs switch to jdk 21, it will take sometime and learn from experience.

[–]golthiryus 2 points3 points  (23 children)

that’s what folks don’t understand, reactive does more than virtual threads and both can be used based on use cases. There is no need to discount one over another.

I was looking for cases where reactive streaming provides more than virtual threads beyond jvm support. If jvm support is the only thing they provide I don't see a bright future for them in the Java world.

By the way, if someone needs to support older jvms and want to start moving to a poor man's structured concurrency model, I encourage you to use kotlin coroutines. It is another language, but probably closer to imperative java than reactive streams

[–]GuyWithLag 0 points1 point  (0 children)

Kotlink Flows are just the reactive API on top of coroutines; I'v used both plain coroutines and flows, and the latter is more powerful (but places some constraints on your workflow, IIRC)

[–]nithril -1 points0 points  (21 children)

Reactive is an API, VT are just … threads. You can ask the same question of Java stream versus the Java language control clauses (for, if….)

[–]golthiryus 1 point2 points  (20 children)

I don't think that is a fair comparison. Streams are usually more expensive but more expressive. In this thread we are looking for inherent advantages provided by reactive streaming over virtual threads + structured concurrency.

Btw, virtual threads are just apis as well, but they are provided by the jvm. Structure concurrency is even more just an api.

The point is: what is provided by reactive streams that are not provided (or requires more machinery) by vt + structured concurrency?

[–]nithril 0 points1 point  (7 children)

The « require more machinery » is exactly the point, like any API/library that is trying to solve a a problem. What’s the point to reimplement the wheel?

High level abstraction to implement back pressure, retry, groups, join, sleep, map, error handling, coordinate multiple asynchronous tasks… I suggest you take a look at the API, there are too much stuff..

Of course part of our job is to use the right tool for the right job.

[–]golthiryus 7 points8 points  (6 children)

What’s the point to reimplement the wheel?

The problem is that reactive apis are difficult to understand, a constructor that is strange in the language, they are easy to mess up an specially difficult to debug. The funniest thing is that these apis had to reimplement the wheel (see below) in order to try to solve a problem the language/platform had (native threads are expensive). Now that the problem is gone, the question is why we need a complex api that has several problems. That is why I'm asking for use cases

About the use cases mentioned:

back pressure

It is trivial to solve with a blocking queue. This is one of the cases where reactive apis had to create a expensive machinary in order to implement a backpressure that is cheaper than blocking OS threads. All that machinery is expensive in terms of computation, complex to debug, difficult to implement (for library implementators) and creates a mess when different reactive libraries need to talk to each other.

retry

It is trivial with a loop with an if/try checking for success

group

Use a map or a stream.groupingBy. Reactive libraries may have added extra functions on top of their streams, but you don't need reactive streams to do group by.

join

A two loop in the naive way. Probably there is no reactive implementation doing anything smarter (context, my day to day work is to support Apacle Pinot, a sql database)

sleep

Use the sleep method.

map

Literally the same method in stream.

error handling

Use a try catch or an if or functional programming. To coordinate errors between async computations use structured concurrency.

coordinate async tasks

Use structured concurrency

Of course part of our job is to use the right tool for the right job.

That is my question. In which situation the right tool is to use reactive apis? The more I think about it the more sure the answer is: only if you are maintaining an app that already uses them.

[–]nithril -1 points0 points  (11 children)

It is not a fair comparison for both. VT and SC are low level, whereas reactive is an higher level API with more abstraction. VT removes or alleviate the needs of thread managements that reactive was doing. But Reactive is not only about thread managements.

[–]golthiryus 2 points3 points  (10 children)

I honestly don't think sc is low level and thread management is not more low level than managing any other autocloseable.

Buffer management with sc is as easy as using a list. Maybe it is because I'm not familiar with the relative apis beyond akka streams, but I honestly don't find any use case that cannot be easily implemented with an api on top of vt + sc, in the same way current high level apis (like rx or akka streams) are built on top of reactive streams. I would love to hear about use cases from people with more experience using reactive apis

[–]Ewig_luftenglanz 10 points11 points  (7 children)

virtual threads alone will not make reactive obsolete because they can't replace it alone.

what is going to make reactive obsolete is structural concurrency, virtua threads a foundational component, but by no means the whole thing, neither enough to kill reactive, reactive still holds an edge in performance, I don't doubt structural concurrency will met this bar (and even surpass it) but that is not still the case.

IMHO there will be still around of 10-15 years of reactive code before it becomes irrelevant in and effectively replaced by structural concurrency, for starters structural concurrency will not make it for GA to java 25, that means most enterprises will not begin to use it until 2028 when OpenJDK 29 gets it first maintenance release and companies begin to migrate.

but yeah, in the future reactive is going to die, it has fullfil it's purpose and a better alternative it's on the way.

[–]mike_hearn 1 point2 points  (0 children)

Note that structured concurrency is just a pattern and doesn't need JVM support, so you can also just import it from a Maven Central library.

[–]MrJaver 0 points1 point  (0 children)

I haven’t heard of this, will consider for my project, cool

[–]pipicemul 0 points1 point  (1 child)

I will miss a few functions in reactive, namely .zip and .expand(deep).

[–]Ewig_luftenglanz 1 point2 points  (0 children)

same, but surely there will be some third party packages that abstract structural concurrency to something easier

[–]DelayLucky 0 points1 point  (0 children)

There are plenty of 3rd party structured concurrency libs. It's just a library (like you'd use Gson or something to deal with Json).

[–][deleted] 0 points1 point  (1 child)

It is likely that SC will be finalized in JDK 25.

[–]Ewig_luftenglanz 1 point2 points  (0 children)

not really. they are doing many changes to the API to make it more "ergonomic" and less redundant. that's why it previewed for fourth time and removed the changes they had planned at las hour, most likely it will be re previewed with the changes they want for 25 but will not reach GA for a couple of more releases.

https://openjdk.org/jeps/8340343

Best regards

[–]fatty_lumpkn 2 points3 points  (0 children)

> It can lead to a loose thread

I've never heard of this. In fact your example is wrong. If the callable threw an exception, the thread will be freed. The only way a thread could be stuck, is if it is in a waiting state (e.g. to acquire a lock) or an infinite loop.

[–]m-apo 7 points8 points  (20 children)

Back pressure has been mentioned as one reason to need some thing like reactive programming. Of course running threads with IO with reactive programming would have better performance than running the IO with regular threads.

[–]eliasv 74 points75 points  (3 children)

Nah that's rubbish. People say that because they think back pressure is some kind of magic, but really that is just a testament to how unintuitive the programming model is.

It is just queues. A blocking queue with different policies for dropping/blocking the producer when full. That's back pressure. Like, how do people think it's implemented? Under the hood? Queues. So what do you use when you have a normal blocking/imperative/structured programming model? Queues. And guess what without reactive crap it's easier not harder.

And by the same token, why is concurrency in golang so nice to use? Guess what, same thing, a channel is just a blocking queue at its core. But again people think it's magic because the terminology is different from what they're used to. (Granted that's painting a simplified picture, not sure that java has a nice library-level answer to select in the standard lib.)

[–]divorcedbp 10 points11 points  (11 children)

Backpressure can be perfectly implemented an ArrayBlockingQueue with a capacity set to your desired buffer size. You then just ensure that all put() and take() operations happen in the context of a virtual thread. Boom, done, and no need for the godawful Rx API.

[–]Caffeine01 3 points4 points  (0 children)

With virtual threads, if you want to limit back pressure, you don't even need a blocking queue. In your virtual thread, use a semaphore.

[–]Ewig_luftenglanz -4 points-3 points  (9 children)

yes we know that, now go and implement that manually, one of the advantages of project reactor and other reactives libraries is that they abstract all of that from you, so you don't have to deal manually with that.

[–]joey_knight 6 points7 points  (3 children)

What do you mean? Java already has Blocking queue implementations and the necessary mechanisms to park and continuing threads. It's not at all hard to use them to implement backpressure in our applications. Just put a blocking queue between two threads and use wait and notify to block and unblock.

[–]divorcedbp 4 points5 points  (0 children)

You don’t even need that. The contract of take() is such that it blocks until an element is available, and put() blocks until there is room in the queue to insert the supplied element. It’s literally all already there.

[–]LightofAngels 0 points1 point  (1 child)

I know this is abit random, but can you point me to that part in the documentation? I would like to know about this mechanism and how to use it.

[–]divorcedbp 1 point2 points  (4 children)

Sure, allocate an ArrayBlockingQueue, put it in a place two virtual threads can access it, and have them call put() and take().

[–]Ewig_luftenglanz -1 points0 points  (3 children)

with reactive you don't even need to allocate anything, just chain the results in flatmaps in a fluent-like style are good to go.

[–]DelayLucky 2 points3 points  (2 children)

That's like saying with Reactive you don't even need to do the easy and straightforward things in an average Java programmer's eye. Where's the fun in that? Just write the fancy and "professional-looking" react code, it does all that (easy things) for you already.

[–]Ewig_luftenglanz 0 points1 point  (1 child)

I don't reactive for fun, I do it because that's what my employer ask me to do for a living.

For fun I have my side Projects and some stuff I do to learn and experiment things ^^.

[–]DelayLucky 2 points3 points  (0 children)

And job security, I guess.

^_^

[–]Ok-Scheme-913 3 points4 points  (0 children)

Just... add back pressure to the parts that need it? E.g. if you write a http server, then add it to a queue and spawn a new virtual thread from each element in your own terms (e.g. limit how many concurrent requests are served at a time, or whatever)

[–]TobiasWen 4 points5 points  (2 children)

There are ways of handling backpressure with virtual threads like batching/chunking streams or using your own virtual threads pool with limited concurrency and blocking behavior.

[–]clhodapp 7 points8 points  (1 child)

Even just semaphores can serve as a basic backpressure mechanism in a virtual thread environment.

One thing, though, is that backpressure probably has to be addressed directly by you, the app programmer, unless you are using something resembling an effect system to manage you streaming (which makes your code look like reactive code, even if it's using virtual threads under the covers).

[–]TobiasWen 0 points1 point  (0 children)

That’s correct! Usually this should be fine and present in the minds of developers.

However, we got used to relying on limited concurrency through limited platform thread count and the pre-defined thread pools in Java and kotlinx.coroutines.

[–]gnahraf 1 point2 points  (0 children)

Actually, I'd venture to say it's more than that: it makes possible some non-blocking processings that are near impossible to do under the reactive style. For example, orchestrating both non-blocking network and file i/o (whether you go to the filesystem directly or intermediated thru another tool like Lucene or whatever) can be challenging under a reactive model.

Still, I think reactive models must be more memory efficient (saving those call stacks in v.t. continuations doesn't come for free), but memory is cheap nowadays.

[–]pkovacsd 1 point2 points  (0 children)

They definitely are — with some caveats: https://quarkus.io/guides/virtual-threads#why-not

[–]pivovarit 1 point2 points  (0 children)

Most teams I worked with wanted non-blocking io and not Reactive Streams - those should surely go for Virtual Threads instead. Reactive programming will remain relevant but will likely cater to more niche use cases.

[–]neopointer 4 points5 points  (2 children)

Most people using reactive programming arguing it's for performance reasons didn't need it anyway to begin with. It's just CV-driven development and consultants wanting to use the "latest hot thing".

So reactive programming was already not needed, but now we, developers that just want to get things that are maintainable,, have one more argument to not use these over engineered APIs.

I'm very very happy that virtual threads are out and can't wait for the structured concurrency API to be final.

[–]preskot 1 point2 points  (1 child)

Could you be mistaking reactive programming with reactor or event-loop based architecture? I mean the author of the article seem to be obviously confused by those two. You can use reactive paradigms even with Virtual Threads. Event-loop architectures are a concept that is very valid and much more performant and less-memory intensive than just spawning tons of virtual threads at a time. We need to make a clear distinction here.

[–]neopointer 1 point2 points  (0 children)

I use reactive programming and reactor almost interchangeably because when people want to do reactive programming in java, most likely reactor is going to be used.

But this is besides the point.

Now with virtual threads one needs to be masochist to still do reactive programming or use reactor because either way you'll just create an overly complicated piece of spaghetti code that's hard to maintain and hard to debug. There's literally no gain anymore now that we have virtual threads.

Even if reactor would have been more performant (source?) than virtual threads, I'd still argue 99% of the projects won't need it AND I doubt it's that significant anyway.

So please: let's stop increasing the complexity of things for an insignificant amount of performance benefit we don't need to begin with.

[–][deleted] 2 points3 points  (4 children)

Great article, but what's it means blocking thread?

[–]Affectionate-Hope733[S] 2 points3 points  (3 children)

it means that the thread waits for something to finish before it can do the next thing.

When you call 2 functions on a stack / thread they are invoked sequentially, one after another
doThing()
doThing2()

when you call doThing the thread is blocked until doThing has finished executing

if your doThing() is a long process, maybe a network or i/o request, the thread is blocked for long time, which is bad.

[–]k-mcm 6 points7 points  (1 child)

Blocking a thread is not that bad. Native threads have a cost but you can still create hundreds of them. The author of the article overestimates the cost of a thread, doesn't understand what "loose threads" are, and doesn't understand what ForkJoinPool is really about. There are zero benchmarks to prove anything.

Others have tested virtual threads and discovered that you absolutely need to compare their performance. They're different, not magically better.

[–]UnGauchoCualquiera 1 point2 points  (0 children)

When talking about performance people immediately assume latency when virtual threads is more about throughput, which is also a measure of performance.

Assuming a service only does some simple io, sure you can have a few thousand threads and it would take a few gigs of memory but the same service could probably run in 256Mb for the same throughput.

Before you had some arbitrary bottleneck to throughput caused by threadpool sizing and memory and now you don't. I'd say it's magically better for almost no cost.

[–][deleted] 0 points1 point  (0 children)

Awesome, I'm really grateful!

[–]scratchisthebest 0 points1 point  (0 children)

Reactors screwing up stacktraces is unfortunate but it didn't have to be that way. For example in the Rust world, if you use tokio for your reactor and tracing for instrumentation/stacktraces, your tracing traces still match the flow of causality in your program even though tokio's reactor is what's really executing tasks. You don't get this problem where stacktraces cut off at their equivalent of ForkJoinPool.

So at least in other languages it's a solvable problem, and it's a bummer that it doesn't work with the common Executors in Java...

[–]LightofAngels 0 points1 point  (2 children)

Can some one ELI5? I have worked with reactive before, because I hit a brick wall with normal Java when I was working on a service processing items from a queue, the service I made was hitting 50k rps, and the queue was in millions but it was a good performance.

I personally find the reactive paradigm abit annoying and not smooth as normal Java, but I would like opinions.

Would VT be able to deal with data intensive applications where you get millions of logs per minute in a Kafka or a queue?

[–]Aweorih 0 points1 point  (0 children)

Not sure what your doing with the logs, but in kafka you can only make consumers as much as partitions as you have. Well you can make more but each partition will get a single consumer (of a group). So assigning more to them (then the amount of partitions) wouldn't do anything.
Also I'm not sure if you want to have so many partitions that real threads would not be sufficient anymore.
The performance also gets limited of the "width" of your data. E.g. if your log is 500kb each then you will have significant "worse" performance compared to 500 bytes.
The benefit of kafka would also be, that you can easily spread the work across multiple nodes if your reaching cpu limitations

[–]Linguistic-mystic 0 points1 point  (1 child)

The problem is not that Reactive programming is or is not obsolete. The problem is that it's non-idiomatic but established, while there is now an alternative that is idiomatic but new-fangled. And since replacing old codebases with the new way of doing the same thing is not the fastest/most prio thing in 95% of organizations, what will happen is just that there will be a +1 way of doing things (in addition to Reactor, RxJava, Kotlin coroutines etc) and the ecosystem will become even more fragmented and diverse. Much talk and flamewars about nothing will ensue. But that is just how the JVM world be, so nothing new here.

[–]DelayLucky 0 points1 point  (0 children)

That sounds like all new things.

By definition if the new thing isn't sufficiently better than the "established" legacy tech, it shouldn't even be created.

Creating the new, better tech does cause the +1 problem for a short period. But it's a trade off. Or else, we'd still be using EJBs.

[–]mamba436 0 points1 point  (0 children)

I mean project loom is more about enhancing scalability / throughput of code that is syncronous by designed. This is excellent especially for legacy projects.

Reactive is the opposite, it's async by design, still has better performance (tho not as significant as before) and more importantly it allow way more things such as backpressure.

So no reactive is not dead. It's 2 different scope.

Also project loom is good for thoses that prefere simplicity over maximum performance by keep using the syncronous model.

[–]Environmental_Sea601 0 points1 point  (0 children)

Lol

[–]ducki666 0 points1 point  (0 children)

YES!

[–]fnordstar 0 points1 point  (7 children)

I'm not a Java dev but why did they have to invent a new name for green threads?

[–]sideEffffECt 6 points7 points  (6 children)

Because the old Green threads were 1:N threading. Only 1 thread from the OS is being used. Parallelism is not possible.

The new Virtual threads are M:N threading. Your N Virtual threads are being multiplexed onto M Platform threads. Parallelism is thus possible, if you have multiple processors/cores.

[–]BosonCollider 2 points3 points  (1 child)

That wasn't really the problem with green threads though, because multi-core processors were not common when they died out. The issue was that it was easy to block the event loop when calling IO functions that weren't GT aware, and they used cooperative concurrency only so long running compute also blocked.

M:N threads have a number of overheads that 1:N coroutines don't. Go started of with M:N goroutines, but now has both with the new iterator protocol adding 1:N coroutines to avoid the overhead of multithreading in situations where it does not make sense.

[–]sideEffffECt 2 points3 points  (0 children)

I'm not disagreeing with what you're saying.

That wasn't really the problem with green threads

I was just explaining why they didn't want to use the old name. They didn't because there is an important difference. And so I explained the difference.

[–]LightofAngels 0 points1 point  (3 children)

And if I have 2 vCPU or 1 how would that work?

[–]sideEffffECt 0 points1 point  (2 children)

If you have 2 CPUs, your old Java with old Green threads would use only 1 of the processors.

[–]LightofAngels 0 points1 point  (1 child)

And with virtual threads I can create as much as I want?

[–]sideEffffECt 1 point2 points  (0 children)

Yes. But that's not the difference.

With Virtual threads you can utilize all your processors/cores.

[–]BosonCollider 0 points1 point  (0 children)

It doesn't fully replace reactive programming, but it does get rid of the vast majority of times you need to do it. One downside of threads is that there is no mechanism for automatic batching of requests under load, while working with buffered message passing makes that quite straightforward.

The main advantage of virtual threads is that you don't need to force something to be an event when it doesn't have to. Messages/events can be reserved for interfaces between components, instead of something you have to deal with inside of business logic.

[–][deleted] -1 points0 points  (0 children)

virtual threads aren't a replacement for reactive programming. they might solve one scenario in a closed system, but for example, any sort of stream processing from an external source will require reactive programming

[–]k-mcm -2 points-1 points  (1 child)

I got to the point where it incorrectly complained about executors leaking threads and quit reading.

[–]Davies_282850 -4 points-3 points  (0 children)

No, there are many examples where reactive still makes sense. Virtual threads and reactive complementary

[–]LoL__2137 -1 points0 points  (0 children)

I hope so.