Java virtual threads hit with pinning issue
infoworld.com
infoworld.com
I’m glad someone else said this, since it means I’m not crazy. I was reading that thinking, man, this sounds like the way it’s outlined to work? Where’s the new issue?
It's also news that they are trying to provide some abstraction to help. Which is great, although as usual I suspect it will need to be applied per situation, not a one-size-fits all.
Java instead did a big thing where they made all the primitives work fine in both cases with one API, something which is honestly really hard and really reflects the level of thought put into modern Java features. But there's a long tail of stuff that's still getting cleaned up (e.g. various weird I/O apis), and honestly I think it _is_ weird that an actual language keyword made it into the long tail.
It's extra fun that it's actually quite hard to hit performance problems as a result of this, as the JVM will actually detect that it's getting to this problem and boot up threads to compensate.
For instance, a quick web search led me to a release notes page (https://www.oracle.com/java/technologies/javase/21-relnote-i...) and a news article (https://www.infoworld.com/article/3689880/jdk-21-the-new-fea...), neither mention anything about synchronized blocks and methods. In fact, the later one says "Previously previewed in both JDK 20 and JDK 19, virtual threads will be finalized in JDK 21.", as if everything now works as expected with virtual threads on Java 21. Which is clearly not the case, if synchronized blocks are enough to make it misbehave. IMO, we can only say virtual threads are really "finalized" once synchronized blocks work as expected (releasing the carrier thread, like AFAIK ReentrantLock already does) when running on a virtual thread.
Kotlin has mutexes that presumably are compatible with coroutines:
kotlinx.coroutines.sync.Mutex
Until Java sorts this out, it could be its "Rust async" watershed moment.But people just… don’t.
Where has this been communicated? The first I heard that you shouldn't use synchronized statements or blocks was in these virtual threads discussions; and the popular IDE I use for Java at work doesn't flag synchronized methods or blocks as something to be avoided. The only thing I had heard about it was that you should avoid synchronizing on objects exposed to users of your library, since it would make it possible for code outside your control to also synchronize on that same object (but IMO synchronizing on arbitrary objects you got from somewhere is itself a bad practice, you should only synchronize on objects you control). And obviously, synchronized methods or blocks do not have the "fair" mode, which might be necessary (or good) in some cases.
(And before Java 9, stack overflows within the implementation of some concurrency constructs could lead to unexpected behavior, see https://openjdk.org/jeps/270 for details; this was never a problem with synchronized methods or blocks.)
There of course are things like ErrorProne that statically check that you unlocked your lock, but there's still bugs possible.
Like, it makes sense that when you synchronized on an object, there's room for contention. It seems weird that synchronizing on an _uncontended_ object can cause contention. But that's what this is. The behaviour is that you have target numCores carrier threads, and if someone synchronizes on an object and then does a non-blocking sleep, it's now blocking, because synchronizing upgraded the non-blocking I/O to blocking.
So basically when you hit this issue, it's because not only has the bad thing been happening, it's also been happening badly enough that all the compensation mechanisms have failed.
It's just weird that a whole language level keyword behaves this badly.
E.g.
for (int i = 0; i < 500; i++) {
newVirtualThread(() -> synchronized (new Object()) {
Thread.sleep(100_000);
});
}
will blow up your JVM (modulo some compensation mechanisms that work definitely kinda) and that's odd.If so, it's pretty much an intractable problem, and is the reason (for example) why Solaris abandoned the two-level thread model and went back to a single level one? https://flylib.com/books/en/2.830.1.14/1/
But then again, as I said, TL;DW ;-)
IMO the compound thing is what makes it be nasty. E.g. you have a function `doSomething` which does some RPC, and that's all nicely non-blocking. But someone called map.computeIfAbsent(x, k -> doSomething(k)), and that uses synchronized on the inside so now your non-blocking API calls all magically became blocking, no further action required.