diff a/test/jdk/java/lang/Thread/virtual/MonitorEnterExit.java b/test/jdk/java/lang/Thread/virtual/MonitorEnterExit.java
--- a/test/jdk/java/lang/Thread/virtual/MonitorEnterExit.java
+++ b/test/jdk/java/lang/Thread/virtual/MonitorEnterExit.java
@@ -22,15 +22,95 @@
  */
 
 /*
  * @test id=default
  * @summary Test virtual thread with monitor enter/exit
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
  * @modules java.base/java.lang:+open
  * @library /test/lib
  * @run junit/othervm --enable-native-access=ALL-UNNAMED MonitorEnterExit
  */
 
+/*
+ * @test id=LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xint-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xint -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xint-LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xint -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1-LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ThreadFactory;
@@ -55,10 +135,11 @@
 import org.junit.jupiter.api.condition.*;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.junit.jupiter.api.Assumptions.*;
 
 class MonitorEnterExit {
+    static final int MAX_VTHREAD_COUNT = 4 * Runtime.getRuntime().availableProcessors();
     static final int MAX_ENTER_DEPTH = 256;
 
     @BeforeAll
     static void setup() {
         // need >=2 carriers for testing pinning when main thread is a virtual thread
@@ -148,10 +229,56 @@
                 assertTrue(Thread.holdsLock(lock));
             }
         }
     }
 
+    /**
+     * Test monitor reenter when there are other threads blocked trying to enter.
+     */
+    @Test
+    void testReenterWithContention() throws Exception {
+        var lock = new Object();
+        VThreadRunner.run(() -> {
+            List<Thread> threads = new ArrayList<>();
+            testReenter(lock, 0, threads);
+
+            // wait for threads to terminate
+            for (Thread vthread : threads) {
+                vthread.join();
+            }
+        });
+    }
+
+    private void testReenter(Object lock, int depth, List<Thread> threads) throws Exception {
+        if (depth < MAX_ENTER_DEPTH) {
+            synchronized (lock) {
+                assertTrue(Thread.holdsLock(lock));
+
+                // start platform or virtual thread that blocks waiting to enter
+                var started = new CountDownLatch(1);
+                ThreadFactory factory = ThreadLocalRandom.current().nextBoolean()
+                        ? Thread.ofPlatform().factory()
+                        : Thread.ofVirtual().factory();
+                var thread = factory.newThread(() -> {
+                    started.countDown();
+                    synchronized (lock) {
+                        /* do nothing */
+                    }
+                });
+                thread.start();
+
+                // wait for thread to start and block
+                started.await();
+                await(thread, Thread.State.BLOCKED);
+                threads.add(thread);
+
+                // test reenter
+                testReenter(lock, depth + 1, threads);
+            }
+        }
+    }
+
     /**
      * Test monitor enter when pinned.
      */
     @Test
     void testEnterWhenPinned() throws Exception {
@@ -211,11 +338,11 @@
      */
     private void testEnterWithContentionWhenPinned() throws Exception {
         var lock = new Object();
         var started = new CountDownLatch(1);
         var entered = new AtomicBoolean();
-        Thread vthread = Thread.ofVirtual().unstarted(() -> {
+        Thread vthread  = Thread.ofVirtual().unstarted(() -> {
             VThreadPinner.runPinned(() -> {
                 started.countDown();
                 synchronized (lock) {
                     entered.set(true);
                 }
@@ -232,10 +359,164 @@
 
         // check thread entered monitor
         assertTrue(entered.get());
     }
 
+    /**
+     * Test that parking while holding a monitor releases the carrier.
+     */
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void testReleaseWhenParked(boolean reenter) throws Exception {
+        assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
+        try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
+            ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
+
+            var lock = new Object();
+
+            // thread enters (and maybe reenters) a monitor and parks
+            var started = new CountDownLatch(1);
+            var vthread1 = factory.newThread(() -> {
+                started.countDown();
+                synchronized (lock) {
+                    if (reenter) {
+                        synchronized (lock) {
+                            LockSupport.park();
+                        }
+                    } else {
+                        LockSupport.park();
+                    }
+                }
+            });
+
+            vthread1.start();
+            try {
+                // wait for thread to start and park
+                started.await();
+                await(vthread1, Thread.State.WAITING);
+
+                // carrier should be released, use it for another thread
+                var executed = new AtomicBoolean();
+                var vthread2 = factory.newThread(() -> {
+                    executed.set(true);
+                });
+                vthread2.start();
+                vthread2.join();
+                assertTrue(executed.get());
+            } finally {
+                LockSupport.unpark(vthread1);
+                vthread1.join();
+            }
+        }
+    }
+
+    /**
+     * Test that blocking waiting to enter a monitor releases the carrier.
+     */
+    @Test
+    void testReleaseWhenBlocked() throws Exception {
+        assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
+        try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
+            ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
+
+            var lock = new Object();
+
+            // thread enters monitor
+            var started = new CountDownLatch(1);
+            var vthread1 = factory.newThread(() -> {
+                started.countDown();
+                synchronized (lock) {
+                }
+            });
+
+            try {
+                synchronized (lock) {
+                    // start thread and wait for it to block
+                    vthread1.start();
+                    started.await();
+                    await(vthread1, Thread.State.BLOCKED);
+
+                    // carrier should be released, use it for another thread
+                    var executed = new AtomicBoolean();
+                    var vthread2 = factory.newThread(() -> {
+                        executed.set(true);
+                    });
+                    vthread2.start();
+                    vthread2.join();
+                    assertTrue(executed.get());
+                }
+            } finally {
+                vthread1.join();
+            }
+        }
+    }
+
+    /**
+     * Test lots of virtual threads parked while holding a monitor. If the number of
+     * virtual threads exceeds the number of carrier threads then this test will hang if
+     * parking doesn't release the carrier.
+     */
+    @Test
+    void testManyParkedThreads() throws Exception {
+        Thread[] vthreads = new Thread[MAX_VTHREAD_COUNT];
+        var done = new AtomicBoolean();
+        for (int i = 0; i < MAX_VTHREAD_COUNT; i++) {
+            var lock = new Object();
+            var started = new CountDownLatch(1);
+            var vthread = Thread.ofVirtual().start(() -> {
+                started.countDown();
+                synchronized (lock) {
+                    while (!done.get()) {
+                        LockSupport.park();
+                    }
+                }
+            });
+            // wait for thread to start and park
+            started.await();
+            await(vthread, Thread.State.WAITING);
+            vthreads[i] = vthread;
+        }
+
+        // cleanup
+        done.set(true);
+        for (int i = 0; i < MAX_VTHREAD_COUNT; i++) {
+            var vthread = vthreads[i];
+            LockSupport.unpark(vthread);
+            vthread.join();
+        }
+    }
+
+    /**
+     * Test lots of virtual threads blocked waiting to enter a monitor. If the number
+     * of virtual threads exceeds the number of carrier threads this test will hang if
+     * carriers aren't released.
+     */
+    @Test
+    void testManyBlockedThreads() throws Exception {
+        Thread[] vthreads = new Thread[MAX_VTHREAD_COUNT];
+        var lock = new Object();
+        synchronized (lock) {
+            for (int i = 0; i < MAX_VTHREAD_COUNT; i++) {
+                var started = new CountDownLatch(1);
+                var vthread = Thread.ofVirtual().start(() -> {
+                    started.countDown();
+                    synchronized (lock) {
+                    }
+                });
+                // wait for thread to start and block
+                started.await();
+                await(vthread, Thread.State.BLOCKED);
+                vthreads[i] = vthread;
+            }
+        }
+
+        // cleanup
+        for (int i = 0; i < MAX_VTHREAD_COUNT; i++) {
+            vthreads[i].join();
+        }
+    }
+
     /**
      * Returns a stream of elements that are ordered pairs of platform and virtual thread
      * counts. 0,2,4,..16 platform threads. 2,4,6,..32 virtual threads.
      */
     static Stream<Arguments> threadCounts() {
