Are there any other actual differences? Better Peformance?
See: Why User-Mode Threads Are Good for Performance https://youtu.be/07V08SB1l8c
Also - millions of Java programmers thank you for not going to async/await. What an evil source-code virus (among other things that is).
I tried to watch it at 1.25x speed as I normally do, but you already talk at 1.25x speed, so no need !
Also, both throughput and latency are performance metrics.
Reading that, it also makes me wonder what happens for disk I/O? Many other runtimes, both "green thread" ones like Golang and asynchronous like libuv/tokio, use a blocking thread pool (static or elastic) to offload these kernel syscalls to because, from what I've read, those syscalls are not easily made non-blocking like e.g epoll is. Does Java Virtual Threads do the same, or does disk I/O block the carrier threads? For curiosity, does Java file APIs use io_uring on Linux if it is available? It is a fairly recently added kernel API for achieving truly non-blocking I/O, including disk I/O. It doesn't seem to bring much over epoll in terms of performance, but has been a boon for disk I/O and in general can reduce context switches with the kernel by reducing the amount of syscalls needed.
[1]: https://inside.java/2021/05/10/networking-io-with-virtual-th...
Yes.
> Does Java Virtual Threads do the same, or does disk I/O block the carrier threads? For curiosity, does Java file APIs use io_uring on Linux if it is available?
We're working on using io_uring where available, especially for filesystem IO. For now, filesystem IO blocks OS threads but we temporarily compensate by increasing the size of the scheduler's worker thread pool.
That measurement told me that it's not necessary to use io_uring for disk I/O performance for some workloads.
It found no improvement in performance from io_uring, compared with a dynamic thread pool which tries to maintain enough I/O-blocked threads to keep the various kernel and device queues busy enough.
This was a little surprising, because the read-syscall overhead when using threads was measurable. preadv2() was surprisingly much slower than pread(), so I used the latter. I used CLONE_IO and very small stacks for the I/O threads (less than a page; about 1kiB IIRC), but the performance was pretty good using only pthreads without those thread optimisations. Probably I had a good thread pool and queue logic, as it surprised me that the result was much faster than "fio" banchmark results had led me to expect.
In principle, io_uring should be a little more robust to different scenarios with competing processes, compared with blocking I/O threads, because it has access to kernel scheduling in a way that userspace does not. I also expect io_uring to get a little faster with time, compared with the kernel I tested on.
However, on Linux, OS threads* have been the fastest way to do filesystem and block-device I/O for a long time. (* except for CLONE_IO not being set by default, but that flag is ignored in most configurations in current kernels),
Interesting, didn't realize the kernel would let you do that. I guess it makes sense since it's up to user space to map pages for the stack. The kernel doesn't have much to do on clone except set the stack pointer.
Just as with performance improvements [1][2][3][4], the actual impact on the user experience is non-linear and often hard to predict. In the case of virtual threads, you go from needing to consciously work around a limited amount of available threads to spawning one per request and moving on.
[1]: https://youtu.be/4XpnKHJAok8?t=3026
[2]: "These tests are fast enough that I can hit enter (my test-running keystroke) and have a response before I have time to think. It means that the flow of my thoughts never breaks." - https://news.ycombinator.com/item?id=7676948
[3]: https://news.ycombinator.com/item?id=37277885
[4]: "Go’s execution tracer has suffered from high overhead since its inception in 2014. Historically this has forced potential users to worry about up to 20% of CPU overhead when turning it on. Due to this, it's mostly been used in test environments or tricky situations rather than gaining adoption as a continuous profiling signal in production." - https://blog.felixge.de/waiting-for-go1-21-execution-tracing...
In my use case, when I tested this, core.async + virtual threads had some slight performance benefits over agents + virtual threads, but since core.async relies on macros the error messages can be hairy.
Thus I prefer to use agents. But my use case involves tens of thousands of these things.
You could always set the backing thread pools for core.async and agents in Clojure. That gives you the ability to use virtual threads right now.
But in order to avoid thread pinning, there will need to be some code changes to convert some uses of synchronized to ReentrantLock. How fast that happens will depend upon the given library maintainer. Here's an issue for making some of these changes in Clojure's core library: https://clojure.atlassian.net/browse/CLJ-2771
I've tested Clojure's agents with the new virtual threads for my targeted use case they're significantly faster than before - I can spin up tens of thousands of mostly idle agents and reach performance close enough to core.async for me.
As it stands, probably won't be heavily used until Java 25.
So why did we create the LTS service? 1. Because the new feature releases, while no more risky than the old ones (like 7u4 and 8u20), do require a little more work that companies don't want to put into legacy applications, and 2. Many companies indeed are willing to pay for more stability for their legacy apps.
So while it is absolutely true that some projects want better stability, this level of stability is new. Companies that religiously stick to old versions now didn't do that in the past. The simplest explanation is that the new release model isn't yet understood, not that thousands of companies changed their risk strategy.
The one argument I've been able to make to get an update is "This has fallen out of support and will no longer get security updates". That seems to be the only motivator for my company to do updates.
Really looking forward to taking advantage of these things (transparently and automatically!) in ZIO/Scala... which I think shows the true power of the JVM-as-platform approach you're taking!
Doesn't cross anyone's mind to _not_ upgrade.
2) other things are deemed to have higher priority.
3) people are satisfied with existing features and don't want to spend energy to upgrade to something that doesn't provide immediate value.
4) folks aren't educated on what the benefit of switching would be so why would it be prioritized? This is a case of "they don't know what they don't know".
I work on a team using Java 8 daily. It's fine. It's got things I wish it didn't (no null in switch statements for example) but I don't care about that so much that I'm going to go through the pain of upgrading 7-9 services in the mono repo, their dependencies, and then test them all to be on a new version of Java.
We ditched spock because of groovy, and never looked back. Now at jdk 21, previously at 20.
One year ago, Gpars already supported Virtual Threads: https://groovy.apache.org/blog/gpars-meets-virtual-threads
As a heavy user of Groovy/Spock, though, I agree that upgrading Groovy itself can be challenging, unfortunately. Really depends though on how many edgy Groovy features you relied on :).
We only used it for Spock AFAIR.
Spring supports new JDK release ("minor version" like you called them, those between 11 and 17 and 21) before release. The only exception was with JDK 13, there was about 2 week slip there.
Lombok (I don't like it) supports every such version at release (not before unfortunately).
Other libs didn't even error out (we keep them at newest versions possible, aside from Jakarta madness).
So from the major libs/frameworks, the only thing that slowed us down was groovy.
But I get that most of the time the issues is with dependencies that upgrade to lazily (that's why I don't like those that are bytecode magic and don't use ASM).
This is a moot point because your the build execution and the project compile/run can be on different JDKs. It is a tiny amount of configuration to decouple them, e.g. to use an EA build.
But Groovy does indeed not work, or has support very late for releases between 8, 11, 17 and 21 - so for anyone that wants to stay current (and not wait 3 or 2 years), using groovy in your code will be a pain - that might be also possible for other JVM languages, but I don't know, haven't used them.
"Gradle now supports using Java 21 for compiling, testing, and starting other Java programs. This can be accomplished using toolchains. Currently, you cannot run Gradle on Java 21 because Kotlin lacks support for JDK 21. However, support for running Gradle with Java 21 is expected in future versions."
https://docs.gradle.org/8.4-rc-1/release-notes.html#support-...
2) no shit. What business user is every in their mind prioritising upgrading their language version? It's not up to them to push the upgrade. It's yours.
3) of course they are. People don't desire what they don't want. Invest in people who are actually interested in improvement of their software.
4)the java team have been pushing heavily via twitter / youtube / infoq / hacker news / other open jdk providers all the new features for every single java version during their 6 months release cycles. If your devs / your team don't know about it, then maybe again youre not encouraging people to want to improve on what they have, or take interest in the tech they work in.
I mean that is fine, do I give a shit what java version in using for my take home salary? No...but I enjoy using the newest, most interesting and useful tools. And you best believe those people are more attractive to other companies and you working on some 15 year old java 8 tech.
2) Sure, pushing and making the decision are not the same thing. I can complain and persuade as much as possible and it doesn't mean it's going to happen.
3) I agree that you want people who care about improving software. Upgrading language versions isn't always the best route to do that though.
4) I don't think everyone on the team is reading about the latest updates in the world of Java. I think a pretty small portion of engineers are keeping 100% up to date, following Twitter accounts for Java dev, watching YouTube videos on it, etc. All that is educational and that's great to know but for most people, it's not going to help them work better because they know about features they can't use.
5) definitely sounds aggressive but okay. I haven't found a company yet who's complained about working in Java 8 versus 11/17. If a company is hiring for a role that uses Java, they're likely not limiting their candidates to those who've used their version of Java. It's a pretty standard language and if you know any other object oriented language, you'll be fine.
Whether you perceive there to be no immediate benefit (hint: there is, Java 8 is an antiquated runtime) or not, delaying upgrading until Java 8 EOL is a way larger risk than upgrading now.
I'm not saying there is no benefit. I'm not saying there is no risk. I agree that going from 8->17 would be worse than 8->11->17.
My point is to list out reasons why a team may not be able to just spend a day upgrading (dependency issues) or why someone might not be given the time to do it.
After an enormously unpleasant debugging cycle, we realized that the JIT compiler was incorrectly eliminating a call to System::arrayCopy, which meant that some fields were left uninitialized. But only when JIT compiled, non-optimized code ran fine.
This left us with three possible upgrade paths:
* Upgrade thrift to a newer version and hope that JIT compilation works well on it. But this is a nightmare since A) thrift is no longer supported, and B) new versions of thrift are not backwards compatible so you have to bump a lot of dependent libraries and update code for a bunch of API changes (in a LARGE number of services in our monorepo...). With no guarantee that the new version would fix the problem.
* File a bug report and wait for a minor version fix to address the issue.
* Skip this LTS release and hope the JIT bug is fixed in the next one.
* Disable JIT compilation for the offending functions and hope the performance hit is negligible.
I ultimately left the company before the fix was made, but I think we were leaning towards the last option (hopefully filing a bug report, too...).
There's no way this is the normal reason companies don't bump JRE versions as soon as they come out, but it's happened at least once. :-)
In general there's probably some decent (if misguided) bias towards "things are working fine on the current version, why risk some unexpected issues if we upgrade?"
The end result of my own investigation led to this quite satisfying thread on hotspot-compiler-dev, in which an engineer starts with my minimal reproduction of the problem and posts a workaround within 24 hours: https://mail.openjdk.org/pipermail/hotspot-compiler-dev/2021...
There's also a tip there: try a fastdebug build and see if you can convert it into an assertion failure you can look up.
If Package A won't run on JDK 17 your entire project is stuck on JDK 11. If Package B is upgraded but has conflicts with Package A, you have to dig through old versions until you find one that works -- and you don't get upgrades.
The more games somebody has played with reflection, undocumented features, deprecations, etc. the more likely you are to have a conflict. And since package managers encourage you to depend on somebody else's code, you end up depending on everybody else's code.
The smaller and greener the project is the more likely it is you can just pull the latest versions and be happy about it. A project that was written when Java 8 was current, and continued to develop, is going to be a nightmare.
The more of someone else's code you use, the more likely one of them bumps into one of the gotchas. And that sets off a cascade of conflicting versions.
1. Use Maven 2. Use BOMs to manage related dependencies 3. No lombok
And 21 brings patterns in switch and records.
A virtual thread thread pool by definition is unbound. If you're binding data to a thread (eg. Thread locals, you now have a seemingly unbound list of threads that is now effectively a memory leak). I bumped into that one a few months ago with Netty that has a per thread cache for some things (thankfully you can turn off that cache). It was creating a significantly large waste of RAM that slowed down the application alone.
The other big one is as I mentioned the synchronized limitation. If you assume naively that anything can run in a virtual thread without worries, you're opening yourself up to deadlocks or at least significantly low performance code if you're relying on libraries/code that are synchronized using java monitors.
There may be more examples of gotchas, but these two are the most notable examples I have right now.
Without a way to trampoline computation (or transform code appropriately) it's probably impractical to do anything like that.
(And of course, still many caveats as the sibling post points out.)
Java has had multi-version jars since 11 I think... that allows library authors to ship code that benefits from new features in newer versions of the JDK while still supporting older ones as well. Hopefully library authors can leverage that, though I'm aware something like Virtual Threads may be very difficult to design around for older versions.
Expect a lot of libraries to start release versions that are java 21 baseline because of this feature alone. We're in for a little bit of dependency hell for the short while. Thankfully, devs have been exposed to a mostly final loom for a year, so my hope is that at least the big projects are well on their way to quick adoptions.
Unlike the 8->11 migration which largely brought pain, the 8->21 release brings with it a ton of value that i think will encourage most shops to actually pull the trigger and finally abandon 8.