Deadlocks in virtual threads - was RFR: 8285196: Deadlock reporting prints carrier thread when virtual thread is in deadlock cycle

Dr Heinz M. Kabutz heinz at javaspecialists.eu
Mon Jun 20 16:16:28 UTC 2022


Hi Ron and Alan,

I was pondering today what would happen if two virtual threads 
deadlocked, either on synchronized or on ReentrantLock. My intuition was 
that if it was on a ReentrantLock, then those two virtual threads would 
be effectively decommissioned, and that they would not use up any 
carrier threads, but that if they were deadlocking on synchronized, that 
the carrier threads would be permanently blocked. This appears to be the 
case, however, due to RFR 8285196 I'm struggling to see how we could 
discover something like this. We'd just notice (or not) that there are 
less carrier threads available. Of course we could dig in further and 
dump all the virtual threads, hoping to discover the deadlock, but the 
tool of the ThreadMXBean does not seem to be available anymore.

Here is an example:

import java.lang.management.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class DeadlockInVirtualThreads {
   private static boolean MONITOR_LOCKS = true;

   public static void main(String... args) throws InterruptedException {
     // var builder = Thread.ofPlatform().name("platform-", 1);
     var builder = Thread.ofVirtual().name("virtual-", 1);
     for (int i = 0; i < Runtime.getRuntime().availableProcessors() / 2; 
i++) {
       deadlock(builder);
       Thread.sleep(50);
     }

     Thread.sleep(1000);
     System.out.println("Started all threads");

     ThreadMXBean tmb = ManagementFactory.getThreadMXBean();
     long[] threads = tmb.findDeadlockedThreads();
     if (threads == null) System.out.println("No deadlocks found");
     else System.out.println(Arrays.toString(threads));

     System.out.println();
     Thread testThread = Thread.startVirtualThread(
         () -> System.out.println("Virtual threads still work")
     );
     testThread.join(50);
     if (testThread.isAlive())
       System.out.println("Virtual threads appear to be broken");
   }

   private static void deadlock(Thread.Builder builder) throws 
InterruptedException {
     var lock1 = new ReentrantLock();
     var lock2 = new ReentrantLock();
     var coop = new Phaser(2);
     var thread1 = lock(builder, coop, lock2, lock1);
     var thread2 = lock(builder, coop, lock1, lock2);
   }

   private static Thread lock(Thread.Builder builder,
                              Phaser coop,
                              Lock lock1, Lock lock2) {
     return builder.start(() -> {
       if (MONITOR_LOCKS) {
         synchronized (lock1) {
           coop.arriveAndAwaitAdvance();
           synchronized (lock2) {
             System.out.println(Thread.currentThread().getName() + " 
locked");
           }
         }
       } else {
         lock1.lock();
         try {
           coop.arriveAndAwaitAdvance();
           lock2.lock();
           try {
             System.out.println(Thread.currentThread().getName() + " 
locked");
           } finally {
             lock2.unlock();
           }
         } finally {
           lock1.unlock();
         }
       }
     });
   }
}

With MONITOR_LOCKS = true, we get the following output:

Started all threads
No deadlocks found

Virtual threads appear to be broken

With MONITOR_LOCKS = false, we get the following output:

Started all threads
No deadlocks found

Virtual threads still work


When instead of virtual threads we use platform threads, we see:

With MONITOR_LOCKS = true and false, we get the same output:

Started all threads
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45]

Virtual threads still work

(Process does not terminate)


What would you consider the best practices for discovering a deadlock 
between virtual threads?

And between a virtual and a platform thread? It appears that in that 
case ThreadMXBean is also silent and does not help diagnose who is 
holding the other lock. For example:


import java.lang.management.*;
import java.util.concurrent.*;

public class Deadlock {
   public static void main(String... args) throws InterruptedException {
     record Monitor(String name) {}
     var monitor1 = new Monitor("monitor1");
     var monitor2 = new Monitor("monitor2");
     var coop = new Phaser(2);
     Thread.ofPlatform().name("platform").start(() -> {
       synchronized (monitor1) {
         coop.arriveAndAwaitAdvance();
         synchronized (monitor2) {
           System.out.println("All's well");
         }
       }
     });
     Thread.ofVirtual().name("virtual").start(() -> {
       synchronized (monitor2) {
         coop.arriveAndAwaitAdvance();
         synchronized (monitor1) {
           System.out.println("All's well too");
         }
       }
     });
     Thread.sleep(100);
     ThreadMXBean tmb = ManagementFactory.getThreadMXBean();
     long[] deadlockedThreads = tmb.findDeadlockedThreads();
     System.out.println("deadlockedThreads = " + deadlockedThreads);
   }
}

Output is:

deadlockedThreads = null

And jstack outputs:


"platform" #30 [26371] prio=5 os_prio=31 cpu=2.80ms elapsed=28.75s 
tid=0x00007fda6b81ea00 nid=26371 waiting for monitor entry 
[0x00007000065f6000]
    java.lang.Thread.State: BLOCKED (on object monitor)
         at 
eu.javaspecialists.course.concurrency.ch10_avoiding_liveness_hazards.Deadlock.lambda$main$0(Deadlock.java:16)
         - waiting to lock <0x000000043fd11c88> (a 
eu.javaspecialists.course.concurrency.ch10_avoiding_liveness_hazards.Deadlock$1Monitor)
         - locked <0x000000043fd11c48> (a 
eu.javaspecialists.course.concurrency.ch10_avoiding_liveness_hazards.Deadlock$1Monitor)
         at 
eu.javaspecialists.course.concurrency.ch10_avoiding_liveness_hazards.Deadlock$$Lambda$14/0x0000000801003be8.run(Unknown 
Source)
         at java.lang.Thread.run(java.base at 19-ea/Thread.java:1596)

"ForkJoinPool-1-worker-1" #32 [39683] daemon prio=5 os_prio=31 
cpu=1.38ms elapsed=28.74s tid=0x00007fda6b930a00 [0x00007000066f9000]
    Carrying virtual thread #31
         at 
jdk.internal.vm.Continuation.run(java.base at 19-ea/Continuation.java:257)
         at 
java.lang.VirtualThread.runContinuation(java.base at 19-ea/VirtualThread.java:213)
         at 
java.lang.VirtualThread$$Lambda$22/0x000000080104e1b0.run(java.base at 19-ea/Unknown 
Source)
         at 
java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(java.base at 19-ea/ForkJoinTask.java:1423)
         at 
java.util.concurrent.ForkJoinTask.doExec(java.base at 19-ea/ForkJoinTask.java:387)
         at 
java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(java.base at 19-ea/ForkJoinPool.java:1311)
         at 
java.util.concurrent.ForkJoinPool.scan(java.base at 19-ea/ForkJoinPool.java:1840)
         at 
java.util.concurrent.ForkJoinPool.runWorker(java.base at 19-ea/ForkJoinPool.java:1806)
         at 
java.util.concurrent.ForkJoinWorkerThread.run(java.base at 19-ea/ForkJoinWorkerThread.java:177)

Would love to hear your thoughts on this.


Regards

Heinz
-- 
Dr Heinz M. Kabutz (PhD CompSci)
Author of "The Java™ Specialists' Newsletter" - www.javaspecialists.eu
Java Champion - www.javachampions.org
JavaOne Rock Star Speaker
Tel: +30 69 75 595 262
Skype: kabutz

On 2022/04/27 18:24, Ron Pressler wrote:
> Do not detect deadlock cycles, be it for monitors only or j.u.c `OwnableSynchronizer`s, when virtual threads are involved in the cycle.
>
> -------------
>
> Commit messages:
>   - Skip carriers in deadlock detection
>
> Changes: https://git.openjdk.java.net/loom/pull/173/files
>   Webrev: https://webrevs.openjdk.java.net/?repo=loom&pr=173&range=00
>    Issue: https://bugs.openjdk.java.net/browse/JDK-8285196
>    Stats: 28 lines in 2 files changed: 9 ins; 14 del; 5 mod
>    Patch: https://git.openjdk.java.net/loom/pull/173.diff
>    Fetch: git fetch https://git.openjdk.java.net/loom pull/173/head:pull/173
>
> PR: https://git.openjdk.java.net/loom/pull/173


More information about the loom-dev mailing list