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!
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.
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
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 !
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...
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.
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.
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.
We ditched spock because of groovy, and never looked back. Now at jdk 21, previously at 20.
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.
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.
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?
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.
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.
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.
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.
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).
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!
you can avoid both issues by using 20yo executorservice.
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.
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.
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?
// 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.
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.