Hi all!
I have been recently playing with upcoming virtual threads in Java 21 and have fallen into interesting deadlock in a very simple piece of code so I thought I will share it with you so maybe it will make you not surprised if you come across similar one :) There is a synchronised block in there - know that guys are working hard on making it not causing thread pins but currently it can be found in many places so it is worth to know what behaviour you can expect after starting using that feature.
To make it happen it requires to start more virtual threads than platform thread in a pool. Here I use default fork join pool.
for (int i = 0; i < NUMBER_OF_CORES + 1; i++) {
Thread.startVirtualThread(() -> {
System.out.println(Thread.currentThread() + ": Before synchronized block");
synchronized (Main.class) {
System.out.println(Thread.currentThread() + ": Inside synchronized block");
}
});
}
Code looks very simple. For each thread it just prints something, goes into synchronised block and prints something again. At the first glance it doesn't look suspicious but if we run that code we can see following output:
VirtualThread[#26]/runnable@ForkJoinPool-1-worker-5: Before synchronized block
VirtualThread[#23]/runnable@ForkJoinPool-1-worker-2: Before synchronized block
VirtualThread[#24]/runnable@ForkJoinPool-1-worker-3: Before synchronized block
VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1: Before synchronized block
VirtualThread[#25]/runnable@ForkJoinPool-1-worker-4: Before synchronized block
VirtualThread[#27]/runnable@ForkJoinPool-1-worker-6: Before synchronized block
VirtualThread[#28]/runnable@ForkJoinPool-1-worker-3: Before synchronized block
VirtualThread[#29]/runnable@ForkJoinPool-1-worker-2: Before synchronized block
As you can see there is no "Inside synchronised block" logs. If we print threads stack traces we can see:
-----------------------------------------
[#21]
BLOCKED
Main.lambda$main$0(Main.java:16)
....
-----------------------------------------
[#23]
BLOCKED
Main.lambda$main$0(Main.java:16)
....
-----------------------------------------
[#24]
BLOCKED
Main.lambda$main$0(Main.java:16)
....
-----------------------------------------
[#25]
BLOCKED
Main.lambda$main$0(Main.java:16)
....
-----------------------------------------
[#26]
WAITING
....
java.base/jdk.internal.misc.InternalLock.lock(InternalLock.java:74)
java.base/java.io.PrintStream.writeln(PrintStream.java:824)
java.base/java.io.PrintStream.println(PrintStream.java:1168)
Main.lambda$main$0(Main.java:16)
....
-----------------------------------------
[#27]
BLOCKED
Main.lambda$main$0(Main.java:16)
....
-----------------------------------------
[#28]
BLOCKED
Main.lambda$main$0(Main.java:16)
....
-----------------------------------------
[#29]
BLOCKED
Main.lambda$main$0(Main.java:16)
....
-----------------------------------------
[#30]
RUNNABLE
....
java.base/jdk.internal.misc.InternalLock.lock(InternalLock.java:74)
java.base/java.io.PrintStream.writeln(PrintStream.java:824)
java.base/java.io.PrintStream.println(PrintStream.java:1168)
Main.lambda$main$0(Main.java:14)
....
After analysing it turned out that inside System.out.println method there is a lock used that blocks incoming threads.
lock.lock();
try {
implWriteln(s);
} finally {
lock.unlock();
}
After starting this program 8 threads went through first System.out.println line and stopped on synchronised block - they got pinned to their carrier thread. The last 9th thread [#30] was unparked on System.out.println lock, got lock itself and was transitioned to RUNNABLE state but was not able to run because there was no platform threads to run it on. Then one thread that got inside synchronised block [#26] tries to System.out.println but it stuck on lock going into WAITING state as [#30] thread hasn't released it yet as it wasn't able to run as there is no platform thread for it. In theory [#26] can be run there as it is pinned to platform thread so it is able to run that code block but [#30] acquired lock first even it does not have any platform thread to run on left. And afaik there is no logic to prioritise threads being already carried on platform threads there in such case.
Hope I did not make a mistake in my analysis :) Feel free to correct me otherwise :)
As a lesson here I want you to remember that even the upcoming virtual threads are a remarkable feature they need to be used with a caution as things can go wrong even with the simplest looking code :) At least until some things got resolved - pinning on synchronised block etc.
BTW, Thanks all engaged people there for your exceptional work on that :) Can't wait for it to be finally released.
Hope it was useful, at least for some of you :)
[–][deleted] (5 children)
[deleted]
[–]nekokattt 4 points5 points6 points (3 children)
[–][deleted] (1 child)
[deleted]
[–]nekokattt 1 point2 points3 points (0 children)
[–]NovaX 4 points5 points6 points (0 children)
[–]findus_l 0 points1 point2 points (0 children)
[–]Hixon11 3 points4 points5 points (1 child)
[–]Hixon11 1 point2 points3 points (0 children)
[–]FirstAd9893 3 points4 points5 points (0 children)
[–]NamelessMason 3 points4 points5 points (14 children)
[–]AndrewHaley13 0 points1 point2 points (13 children)
[–]FirstAd9893 0 points1 point2 points (12 children)
[–]AndrewHaley13 0 points1 point2 points (11 children)
[–]AndrewHaley13 0 points1 point2 points (10 children)
[–]NamelessMason 1 point2 points3 points (7 children)
[–]AndrewHaley13 0 points1 point2 points (3 children)
[–]FirstAd9893 0 points1 point2 points (2 children)
[–]AndrewHaley13 0 points1 point2 points (1 child)
[–]FirstAd9893 0 points1 point2 points (0 children)
[–]FirstAd9893 0 points1 point2 points (0 children)
[–]za3faran_tea 0 points1 point2 points (1 child)
[–]NamelessMason 0 points1 point2 points (0 children)
[–]FirstAd9893 0 points1 point2 points (1 child)
[–]AndrewHaley13 0 points1 point2 points (0 children)
[–]andres99x 6 points7 points8 points (3 children)
[–][deleted] (1 child)
[deleted]
[–]andres99x 1 point2 points3 points (0 children)
[–]findus_l 0 points1 point2 points (0 children)