Hacker News new | past | comments | ask | show | jobs | submit login

How much old advice on Java concurrency will virtual threads change?

I'm building a course for people who are new to Java concurrency and I've made the assumption that from September, you will almost always _choose_ virtual threads over platform threads. The default virtual thread management surely matches what most people want most of the time (1 worker thread per CPU core, virtual threads on top). It removes a lot of the old worries about management of thread pools and how OS thread implementation details "leak" into Java.

It's brought it much closer to how I'd written coaching material for multithreading in Go, but the Executor abstractions will still let Java programmers shift over from platform threads at their own pace.




Wrangling executor services (practically) becomes an antipattern. Where old java you'd do a singleton threadpool, virtual thread java you'd pretty much never do that. In cases where you might be tempted to do that, the common fork join pool is likely the better choice.

Locks become more relevant. Old java you might have limited concurrency using thread pool size. With virtual threads something like a blocking queue or semaphore is likely a better choice.

Reactive java should be avoided (IMO). It was a decent choice for old java, for virtual thread java you get little benefits from it.

This will probably change, but with old java `synchronized` was alright. Virtual thread java you want to avoid that.

This likely won't change, ThreadLocalStorage is a bad idea with virtual threads, you almost certainly want to use ScopedValues instead.

You should understand the implications of structured concurrency. It's not fully ready yet, but at very least the `try(var service = newVirtualThreadPoolExecutor)` does work (I believe) which will block until all tasks launched are completed. This is likely desirable but might be unexpected if you wanted to fire and forget something.

Otherwise, I think all the old concurrency advice applies.


> Reactive java should be avoided (IMO). It was a decent choice for old java, for virtual thread java you get little benefits from it.

This is interesting, we used RxJava quite a bit on Android until Kotlin Coroutines made things a bit easier in that the code read more linearly and worked nicely with built in language features like exceptions for error handling.

Just curious if you agree with this in Java, or had other reasons.


Even before the introduction of virtual threads RxJava should have been avoided.

It colors your entire code base, prevents natural error handling, and quite frankly makes maintenance that much more complex.

Obviously there’s a place for it, but every project I’ve been on that used it shouldn’t have.


I'm curious about this from the perspective of a functional Kotlin backend (using Arrow or not).

Do virtual threads make reactive within that model unnecessary too? Seems maybe not? Reactive is both a model, and a paradigm, no?

Even with virtual threads, there will still be a reason to write reactive programs it seems.


I think it's (looesely) tasks vs threads. Threads have a stack trace and a well defined state in the JVM, tasks are objects on the heap.

I work on moderate sized async reactive pipeline with lots of CompletableFutures throughout (and chaining of them so you can process a batch in parallel and commit contiguous offsets periodically) and there's several bugs that have been undebuggable because they happpen _infrequently enough_ in prod, the heap dump is huge, and there's no stack trace to tell you what is going on. The best you can do is lots lots and lots of logging and turn it into an analytics problem to find out what happened (to be fair, logging is only marginally better than wading through a heap dump of completable futures).


There's going to be a difference in the way you do things. Most of the completable future API is going to be unnecessary, .join will work better as there's not penalty for blocking.

I've made my fair share of completable future messes with a lot of biconsume/thenApply/handle nonsense to try and avoid blocking.


The reason to prefer kotlin coroutines is exactly the reason to ditch reactive for virtual threads.

The only difference is virtual threads have platform support which means even better stack traces and no need to decorate your method.

So yup, agree.


It will be years when most Android users are on Java 21. It’s also not a feature that can be transpiled. So for Android developers virtual threads are probably not a thing we need to understand or use in any near future.


Why stop using 'synchronized'?


`synchronized` is a lock without any ordering/fairness guarantee. Therefore, with the increased concurrency caused by Virtual Threads, there's higher chances for starvation.

`ReentrantLock` pretty much replaces every use case of `synchronize` and also supports `fairness`. The only downside of `ReentrantLock` is making the code more verbose, and inexperienced programmers might forget to release the lock.


More than that (as referenced above), `synchronized` currently pins a virtual thread to its carrier. So if you had 4 carrier threads, you only need to have 4 synchronized methods in flight that depend on the action of a 5th to hang the JVM.


Got it, so for now using `synchronized` actually prevents the concurrency benefits of virtual thread because it pins the carrier thread. Plus, there's also the possibility of deadlock when mixing `synchronized` and `j.c.u.Lock`.


Pretty much. You won't die if you run over some synchronized code (like ConcurrentHashMap) but you do have to think about what's happening when you hold that lock.

From the mailing list it sounds like the plan is to fix this, I hope that happens sooner rather than later.


I don't think VTs should be used in practice until Java 22, when it is likely that synchronization (monitors) will be supported [1]. A lot of code may synchronize and be non-trivial to convert to a ReentrantLock, such as ConcurrentHashMap's computeIfAbsent. This can lead to surprising failures outside of the application author's control, such as [2].

Go was originally non-preemptive ("the loops problem"). This was changed in 2019 to preempt at safepoints [3]. The Java team will probably have to do this too [4], but similarly it is low on their priority list and requires feedback.

java.util.concurrent is still the basis of good Java concurrency. Some external libraries for reactive or actors might become less popular, but they were always the minority. There will be some improvements with structured concurrency to make that a little bit easier, but the fundamental approach will remain the same. I think old advice will be relevant, except when it comes to thread pool sizing (which was always a black art). At best some over eager use of async will go away and we'll return to simpler blocking code.

[1] https://github.com/openjdk/loom/commit/24e80bdee3eaf347f2eab...

[2] https://mail.openjdk.org/pipermail/loom-dev/2023-July/005990...

[3] https://go.dev/src/runtime/preempt.go

[4] https://github.com/openjdk/loom/tree/preempt-support

[5] https://openjdk.org/jeps/453


Were original Java green threads preemptive (and at safepoints)?


It is sad that Googling didn't find the answer by ChatGPT did.

---

Here's an excerpt from the book "Java Threads" by Scott Oaks and Henry Wong, published by O'Reilly Media, which discusses green threads:

"Green threads provide a thread abstraction to the application, allowing the programmer to write multithreaded programs without worrying about the underlying OS support. However, because they're implemented at the user level, they are not subject to preemptive multitasking by the underlying OS. Instead, they must voluntarily yield control to other threads. If a green thread fails to do this, it can effectively freeze all other green threads in the system."

This excerpt supports the fact that green threads were not preemptive and relied on voluntary cooperation to switch control between threads.


Thanks! I remember that book, but i do not have it.


Only for IO bound tasks, if you are doing a compute bound task that needs to saturate all cores, I am not sure what benefits virtual threads would bring.


The trade off is going to remain the same for parallel algorithms, once saturated performance will gradually decline as swapping threads harms performance. Go for example exposes the CPU Core/thread count because unbounded performs worse even despite its apparently cheap virtual threads. I have tested this a number of times in Java and Go and the situation is similar you want to be using near the CPU count dependent on the algorithm and IO. It will be the same in Java with virtual thread, the cost of the threads will be lower but the optimal number likely wont change much.


In Java, unless they changed something in the past ~6 months, virtual threads will not "swap out" a CPU bound task, you have to have some blocking, otherwise it will keep on chugging.

Depending upon your POV this may be a positive or negative. You can get around it by manually inserting some sleeps() - note that yield() won't work (which is kinda unfortunate).


Does sleep(0) also work?


At the moment (as far as I know), only for NETWORKING bound tasks. Not general IO. Java at the moment doesn't support asynchronous file IO, this makes some sense since async-io on Linux is a bit of a mess still in terms of stability and security (but it's getting there so fingers crossed...).

E.g. I'm building an app that does a lot of file IO and I won't be using virtual threads. They would provide nothing for this particular use case.


Depends a lot on what you are doing and if you need preemption. There's not a huge penalty for using a virtual thread for compute (Other than impacting IO bound tasks).


> Only for IO bound tasks

do you know any docs/examples how this can be integrated with current java io libraries?.


So I subscribe to the belief that a Java multi-thread application should have at least 2 pools: 1 bounded pool for CPU-heavy tasks and 1 unbounded pool for IO tasks. I'll probably change the IO-pool to using virtual threads now, while keeping the CPU-pool on real thread.


Now would be a good time for an revised edition of JCIP (Java Concurrency in Practice).


For sure, yes, it's from 2006! I didn't have that one to hand, but remember the O'Reilly "Java Threads" book from 1999 and >50% of that was still useful for bringing a younger colleague up to speed.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: