Java 21: The Nice, the Meh, and the Momentous(horstmann.com) |
Java 21: The Nice, the Meh, and the Momentous(horstmann.com) |
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.
As it stands, probably won't be heavily used until Java 25.
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!
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.
Are there any other actual differences? Better Peformance?
See: Why User-Mode Threads Are Good for Performance https://youtu.be/07V08SB1l8c
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...
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.)
Doesn't cross anyone's mind to _not_ upgrade.
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.
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.
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?"
1. Use Maven 2. Use BOMs to manage related dependencies 3. No lombok
And 21 brings patterns in switch and records.
I would love if Java pattern matching could at least get to the level of ruby pattern matching. Ruby pattern matching will allow you to deconstruct arrays and hashes to get pretty complicated patterns, which is really powerful. Right now it seems like Java might have that with a lambda in the pattern, but its not going to be as elegant as ruby where:
case {name: 'John', friends: [{name: 'Jane'}, {name: 'Rajesh'}]} in name:, friends: [{name: first_friend}, *] "matched: #{first_friend}" else "not matched" end #=> "matched: Jane"
But the big change here is virtual threads which should be a game changer.
We have a hundreds of third party dependencies across the code base, a lot of the big ones (Hibernate, Spring, a lot of Apache). We write a big web application and maintain a big legacy desktop application in Swing.
We run a dedicated nightly CI job that is on the latest Java release to get early warning for any incompatibilities. After the painful migration from 8 to 9 so many years ago it has been smooth sailing.
In all those version upgrades over all those years and dozens of on premise installations with big customers we have never had a regression or a problem that was caused by the runtime itself.
(2) I've looked at other ways to extend the collections API and related things, see
https://github.com/paulhoule/pidove
and I think the sequenced collections could have been done better.
(3) Virtual Threads are kinda cool but overrated. Real Threads in Java are already one of the wonders of the web and perform really well for most applications. The cases where Virtual Threads are really a win will be unusual but probably important for somebody. It's a good thing it sticks to the threads API as well as it did because I know in the next five years I'm going to find some case where somebody used Virtual Threads because they thought it was cool and I'll have to switch to Real Threads but won't have a hard time doing so.
What is with this awful formatting? https://i.imgur.com/nQmt7Qo.png
Dunno, several of these are tangible QoL boosts:
Math.clamp(), List.reversed(), List.addFirst(), List.addLast(), Character.isEmoji()
These fall under sequenced collections, not "miscellaneous new methods".
Google is not helping.
Completely readable at 100% width though.
But I tend to use reader mode on most sites anyway because it's an easy way to get rid of banners (cookies, subscription etc.)
Any modern web app already has multiple instances of the app querying a db, so you have to keep a tally of total connection number either way.
How do I reason about the order in which the calls change the state of the world?
I'm aware of `permits` clause, but it's not good enough.
Do those exist?
Going to be interesting!
If you don't trust the Oracle based open source builds then just wait a bit for Microsoft, Redhat, and others to release their version 21 OpenJDK builds that will be found under https://adoptium.net/marketplace/
> "Hello, World!".splitWithDelimiters
> ("\\pP\\s\*", -1)
> // ["Hello", ", ", "World", "!", ""]
> MehMy brain just melted.
Most of which were likely introduced during new feature development in recent releases. To suggest that this on its own somehow manifests a more stable jdk compared to some ancient, battle tested version of the jdk is debatable.
I find it rather concerning that so many bugs exist to begin with. Why are these not caught sooner?
Has the whole world gone crazy? Am I the only one around here who gives a shit about quality? Mark it zero!
https://bugs.openjdk.org/browse/JDK-8316305?filter=-7&jql=pr...
Being allergic to JIRA, my JIRA-fu is weak, so there's probably an easier/faster way to report bugs fixed in v21.
Any way.
> Am I the only one around here who gives a shit about quality?
Ages ago, I was a QA/Test manager. So I appreciate your sentiment. But it seems to me that Oracle's being a FANTASTIC shepherd of Java. Definitely a huge upgrade, at the very least.
As to why some bugs go unnoticed for long, if you look at the bug database for reports of bugs that have been effect for a long while you'll see that these are almost always rather extreme corner cases (or, more precisely, the more utilised a mechanism is, the more extreme would be its old bugs). That's simply because full coverage is simply infeasible for software of such size (~8MLOC); you see similar bug numbers for the Linux kernel. The largest software that can be shown to be free of bugs is currently on the order of 10KLOC, so if your software is much larger than that and isn't getting many bug reports it's probably because it's not used that much.
Virtual threads are going to make Ruby fibers work properly for JRuby so that’s going to be huge as well.
Charles Nutter gave an update in August. 45 minute mark he talks about virtual threads.
print(switch ({'name': 'John', 'friends': [{'name': 'Jane'}, {'name': 'Rajesh'}]}) {
{'friends': [{'name': var firstFriend}, ...]} => "matched: $firstFriend",
_ => "not matched"
});
Pretty similar! The main differences are that Dart doesn't have symbols, so the keys are string literals instead. Also, variable bindings in patterns are explicit (using "var") here to disambiguate them from named constant patterns.[1]: https://medium.com/dartlang/announcing-dart-3-53f065a10635
I've been using that and I love it, in general... but can I ask you why do we need to name a variable in a pattern like this:
switch (p) {
Person(name: var name) => ...
}
That's the only thing that feels a bit annoying as you have to rename the variable...
In Java, this would be something like: Person(var name) -> ...
EDIT: I guess it's to support `Person(name: 'literal')` matches.> Dart doesn't have symbols
That's weird, as I actually use sometimes `#sym` (which has type `Symbol`)??
print((#sym).runtimeType);
This prints `Symbol` :)I know you know Dart in and out, but could you explain why this is not actually a symbol in the way Ruby symbols are?
When I teach Scala, a very high percentage of the teaching time is ultimately down to re-introducing how to design business domains, because seasoned devs just reach for large classes with a million optional fields, which not only can represent valid systems states, but thousands of invalid ones.
I’d rather see a boatload load of other features before patterns. I’ve been experimenting with project manifold[1]. _That_ is the path Java sb on. Just my take.
We've had two production bugs in the last two weeks caused by handlers blocking the server thread in apps using an async web framework, which would simply not have happened with a synchronous server.
The problem with regular threads is (a) multi-kb memory stack per thread and (b) consuming a file handle.
Either of those severely limits the scalability of the most "natural" parallelism constructs in Java (perhaps generally). Whole classes of application can now just be built "naturally" where previously there were whole libraries to support it (actors, rxJava, etc etc).
It make take a while for people to change their habits, but this could be quite pervasive in how it changes programming in general in all JVM languages.
What do you mean by using a file handle, is this a Windows platform thing? On *ix, threads don't use up file descriptors (but you can still have a million fd's at least on linux for other stuff if you want).
you can avoid both issues by using 20yo executorservice.
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.
// client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
// .thenApply(HttpResponse::body)
var body = response.body();
// .thenApply(this::getImageURLs)
var urls = getImageURLs(body);
// .thenCompose(this::getImages)
var images = getImages(urls);
// .thenAccept(this::saveImages)
saveImages(images);
And if it had been written this way it would have been clearer that they are, in fact, equivalent. But generally people don't write like this, they use looping constructs.Regardless, the important bit is that the parallel/concurrent bit of the async one is that it is cast off into an async system. The following execution steps are, well, steps. Each executed in sequence. Just like the body of the virtual thread example would be executed, but without the cumbersome noise of thenApply and thenCompose and such.
An 'academic' language if ever there was one.
But I recall it as the first vaguely Erlang-like language on the JVM, so whenever something about threading comes up I recall it.
I'm learning Elixir instead.
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.
Thanks - this caused me to dig into the specific scenario where creating threads was exhausting file handles in my experience and you are correct - consuming a file handle is indeed not intrinsic to creating a new thread in Linux. It's insanely easy for literally anything you do with the thread to consume a file handle, but of course, that applies to virtual threads as well. Thanks!
If these threads are handling TCP connections and L7 protocol processing on top, you're going to have nontrivial both kernel and userspace memory usage per connection too that may dwarf the thread overhead.
Here's a linux kernel dev (Ingo Molnar) benchmarking Linux in 2002 and starting just shy of 400k threads in 4 GB: https://lkml.iu.edu/hypermail/linux/kernel/0209.2/1153.html - though on a 32 bit systems lots of objects things are 50% the size compared to current 64 bit. But still gives you a ballpark.
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.
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.
you can still wrap the medium speed/slower stuff in virtual threads.
const pi = 3.14; // Close enough.
switch (value) {
(pi, var pi) => ...
}
This case matches a record whose first field is equal to 3.14 and binds the second field to a new variable named "pi". Of course, in practice, you wouldn't actually shadow a constant like this, but we didn't want pattern syntax to require name resolution to be unambiguous, so in contexts where a constant pattern is allowed, we require you to write "var", "final", or a type to indicate when you want to declare a variable.Swift's pattern syntax works pretty much the same way.
> > Dart doesn't have symbols
> That's weird, as I actually use sometimes `#sym` (which has type `Symbol`)??
Oh, right. I always forget about those. Yes, technically we have symbols, but they are virtually unused and are a mostly pointless wart on the language. It's not idiomatic to use them like it is in Ruby.
I'm not sure exactly what you mean by "need" here, but as far as I know, Dart doesn't make any promises about the memory management or efficiency of either strings or symbols.
If I were you, I'd just use strings.
Luckily, the current Dart specification does guarantee this (section 17.8):
"Assume that i ∈ 1, 2, and that oi is the value of a constant expression which is a symbol based on the string si. If s1 == s2 then o1 and o2 is the same object. That is, symbol instances are canonicalized."
Apparently, there's even special treatment for "private symbols", which are only the same object "in the same library". TIL.
Source: https://spec.dart.dev/DartLangSpecDraft.pdf
EDIT: there's even a whole sentence justifying the existence of symbols as being related to reflection, actually... they say Dart literal Strings are already "canonicalized" so that fact about Symbols is not enough for that.... hence you're right that String literals are just as good for the use-cases I had in mind. I guess I will use String literals from now on after all.
EDIT 2: > I'm not sure exactly what you mean by "need"
Hopefully it's clear what I "needed" now... basically, interned Strings to avoid wastefully comparing bytes when a pointer comparison would suffice as all values are known at compile-time.
Here's what I mean. The Ruby will throw NoMatchingPatternError and the Python will silently do nothing.
x = [10, "figs"]
case x
in [n, "apples"]
:foo
in [n, "oranges"]
:bar
end
# ---
x = [10, "figs"]
match x:
case [n, "apples"]:
...
case [n, "oranges"]:
...I recently spotted a (new to me) foreach / else construct in a templating language (sorry, forget which one); else is invoked if the list is empty. Nice sugar for common outputs like "no items found".
I appreciate modest syntactic sugar.
For instance, my #1 sugar wish is for Java's foreach is to do nothing when the list reference is null. Versus tossing a NPE.
Eliminates an unnecessary null check and makes the world a little bit more null-safe.
for / else should do that too …
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.
Also is that VT allocation more than all of the extra allocations from reactive frameworks internally? Or all of the heap capturing lambdas that you pass to reactive libraries? Do you have a source comparing any of this?
One object per stack frame of a virtual thread is cheaper than one callback object per suspension point.
I like Ruby but I wouldn’t use it for the things I use Python for.
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),
var f = async_api_returns_future();
...
var res = f.join();
but join() won't block OS/JVM thread, but make it to perform other tasks in the queue.
Or you can design API which will receive executorService as params, and run callback there, e.g.:
async_call(Callable callback, ExecutorService threadPool);
Moreover, the platform now has no insight about your composition. Exceptions, which are designed to give context in the form of a thread stack trace, simply don't know about the context as it's not composed through the normal composition (in plain terms, stack traces in asynchronous code don't give you the operation's context). Debuggers cannot step through the asynchronous flow because the platform's built in debugging support works only by stepping through threads, and profilers are no longer able to assign IO to operations: a server that's under heavy load may show up as idle thread pools in a profiler because the platform cannot assign an asynchronous operation to some asynchronous pipeline such as CompletableFutures because these are not observable constructs of the Java platform.
Virtual threads give you the same scalability as asynchronous code but in a way that fits with the design of the Java platform. All language constructs work and compose well, debuggers step through code, and profilers can understand what's going on.
That's not to say that some other platform could not be designed around a different construct, but the Java platform -- from language, through libraries and bytecode, and all the way to the VM and its tooling interfaces -- was designed around the idea that sequential composition occurs by sequencing operations on a single thread. And virtual threads are just Java threads.
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.
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.
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.
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.
"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-...
BTW, as for fork-join's `join`, not only is it designed for pure computation only and cannot help with IO, but every `join` increases the depth of the stack, so it is fundamentally limited in how much it can help. FJ is designed for pure computation workloads, so in practice that's not a huge limitation, but virtual threads are designed for IO workloads.
I apologise for not going into more depth here, but as you can imagine, with a user base numbering in the many millions, we can only afford to put in depth explanations in texts and videos that gain a large audience, but once you've familiarised yourself with the material I'll gladly answer specific questions (and perhaps your questions will help us improve our material, too).
my concern is that you somehow can find time to write long comments with lots of handwavings (our framework is designed for that, and their framework is not designed for that), but refuse to provide specific code pointers and example in support of your opinion. For example, in this specific case, can you give example how green threads can be used with current Java IO library, or Java JDBC library?
If there's something unclear in that material, please ask. Also, there is no our framework vs. their framework here. I'm only discussing the JDK's own various thread pools vs. virtual threads. They were all designed and implemented by the JDK team.
BTW, I'm not trying to support any opinion. Pretty much all popular Java server frameworks are adopting virtual threads, and Java's virtual threads are poised to become the most popular lightweight user mode threads. We've already convinced everyone that needed convincing. I'm merely offering pointers in case you're interested to learn how to use virtual threads and understand how they offer high throughput and good observability at the same time (whereas before you could have one or the other). Of course, if you're satisfied with the throughput and observability you can get with our old mechanisms, you don't have to use virtual threads. We've not taken anything away.
We did spend some time contemplating teaching the platform about non-thread-based, i.e. asynchronous sequential composition, in the end we realised that if it walks like a thread and quacks like a thread, we might as well call it a thread.
If you read the JEP and play around with virtual threads (e.g. do asynchronous IO with CompletableFuture or blocking IO in a virtual thread and see what their exception stack traces look like and what their JFR profile looks like) you'll quickly see that the capabilities they offer were simply not attainable by asynchronous code, which is why we've spent years to teach the JVM's innermost mechanisms to be able to observe virtual threads and expose them to observability tools the same way as platform threads are (and how I know those capabilities weren't available before).
We've written and presented a significant amount of published material about virtual threads so there's not much point in recreating it here, but if you're interested, all that material is out there.
[1]: https://openjdk.org/jeps/444
[2]: https://docs.oracle.com/en/java/javase/21/core/virtual-threa...
- 95% java business spaghetti code doesn't require such scalability and fine with spawning of 10k threads on modern hardware
- in 5% left cases, 80% can be covered by executorservice and forkjoinpool
- in 1% cases which left, engineer made wrong decision in choosing JVM because of its other many performance issues
The fact that you can't bring simple code example and quality of your previous comments make me think that you not necessary understand what are you doing.
As I said, I've put examples and detailed explanations in a significant amount of material that's available online that will help you understand how and why user mode threads work and why we decided to add them to Java. While I can't teach concurrency and the design of the Java platform from the ground up (especially detailed mechanisms such as stack walking, JVM TI and JFR) on an individual basis on social media, I'd be happy to answer any specific questions you may have once you've familiarised yourself with the subject.
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).
try (var in = url.openStream()) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); }
which claims that this example will scale well with virtual threads, my understanding is that in.readAllBytes() will call OS blocking socket API underneath, which will block OS thread, so you would need many OS threads to scale. Is this understanding correct?
You can have a million threads blocking on a million sockets (obviously without creating a million OS threads): https://github.com/ebarlas/project-loom-c5m
You can't do that with thread pools. You could achieve that scalability with async code, but then observability tools will not be able to track the IO operations and who initiated them, but with virtual threads you'll see exactly what business operation is doing what IO and why.
it looks like it is true for several API you implemented support for. What about other API, for example some JDBC driver which wants to use non-blocking DB driver. How to use virtual threads with that?
my point is that you can't teach because you don't have expertise and getting lost on extremely basic things: https://news.ycombinator.com/item?id=37620046
JDBC drivers that are implemented on top of their own native code are a different matter, but they are not common these days.
He had worked on "thread stuff" before working on loom on the JVM. I did a search and that's 10 years ago.
https://web.archive.org/web/20130601144756/https://blog.para...