/* * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import jdk.test.lib.Asserts; import jdk.test.lib.Utils; import jdk.test.lib.process.ProcessTools; import jdk.test.lib.process.OutputAnalyzer; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.regex.Matcher; import java.util.regex.Pattern; /* * Tests that JNI monitors work correctly with virtual threads, * There are multiple test scenarios that we check using unified logging output * (both positive and negative tests). Each test case is handled by its own @-test * definition so that we can run each sub-test independently. * * The original bug was only discovered because the ForkJoinPool worker thread terminated * and trigerred an assertion failure. So we use a custom scheduler to give us control. */ /** * @test id=normal * @bug 8327743 * @summary Normal lock then unlock * @library /test/lib * @modules java.base/java.lang:+open * @requires vm.continuations * @run driver JNIMonitor Normal */ /** * @test id=multiNormal * @bug 8327743 * @summary Normal lock then unlock by multiple threads * @library /test/lib * @modules java.base/java.lang:+open * @requires vm.continuations * @run driver JNIMonitor MultiNormal */ /** * @test id=missingUnlock * @bug 8327743 * @summary Don't do the unlock and exit normally * @library /test/lib * @modules java.base/java.lang:+open * @requires vm.continuations * @run driver JNIMonitor MissingUnlock */ /** * @test id=multiMissingUnlock * @bug 8327743 * @summary Don't do the unlock and exit normally, by multiple threads * @library /test/lib * @modules java.base/java.lang:+open * @requires vm.continuations * @run driver JNIMonitor MultiMissingUnlock */ /** * @test id=missingUnlockWithThrow * @bug 8327743 * @summary Don't do the unlock and exit by throwing * @library /test/lib * @modules java.base/java.lang:+open * @requires vm.continuations * @run driver JNIMonitor MissingUnlockWithThrow */ /** * @test id=multiMissingUnlockWithThrow * @bug 8327743 * @summary Don't do the unlock and exit by throwing, by multiple threads * @library /test/lib * @modules java.base/java.lang:+open * @requires vm.continuations * @run driver JNIMonitor MultiMissingUnlockWithThrow */ public class JNIMonitor { public static void main(String[] args) throws Exception { String test = args[0]; String[] cmdArgs = new String[] { "-Djava.library.path=" + Utils.TEST_NATIVE_PATH, // Grant access to ThreadBuilders$VirtualThreadBuilder "--add-opens=java.base/java.lang=ALL-UNNAMED", // Enable the JNI warning "-Xcheck:jni", "-Xlog:jni=debug", // Enable thread termination logging as a visual cross-check "-Xlog:thread+os=info", // We only count monitors in LM_LEGACY mode "-XX:LockingMode=1", // Disable compact headers since that switches locking mode to LM_LIGHTWEIGHT "-XX:+UnlockExperimentalVMOptions", "-XX:-UseCompactObjectHeaders", "JNIMonitor$" + test, }; OutputAnalyzer oa = ProcessTools.executeTestJava(cmdArgs); oa.shouldHaveExitValue(0); oa.stdoutShouldMatch(terminated); switch(test) { case "Normal": case "MultiNormal": oa.stdoutShouldNotMatch(stillLocked); break; case "MissingUnlock": oa.stdoutShouldMatch(stillLocked); break; case "MultiMissingUnlock": parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT); break; case "MissingUnlockWithThrow": oa.stdoutShouldMatch(stillLocked); oa.stderrShouldContain(throwMsg); break; case "MultiMissingUnlockWithThrow": parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT); parseOutputForPattern(oa.stderrAsLines(), throwMsg, MULTI_THREAD_COUNT); break; default: throw new Error("Unknown arg: " + args[0]); } oa.reportDiagnosticSummary(); } // The number of threads for a multi tests. Arbitrarily chosen to be > 1 but small // enough to not waste too much time. static final int MULTI_THREAD_COUNT = 5; // The logging message for leaving a monitor JNI locked has the form // [0.187s][debug][jni] VirtualThread (tid: 28, carrier id: 29) exiting with Objects still locked by JNI MonitorEnter. // but if the test is run with other logging options then whitespace may get introduced in the // log decorator sections, so ignore those. static final String stillLocked = "VirtualThread \\(tid:.*exiting with Objects still locked by JNI MonitorEnter"; // The carrier thread termination logging has the form: // [1.394s][info][os,thread] JavaThread exiting (name: "pool-1-thread-1", tid: 3090592). static final String terminated = "JavaThread exiting \\(name: \"pool-1-thread-1\""; static final String throwMsg = "Terminating via exception as requested"; // Check the process logging output for the given pattern to see if the expected number of // lines are found. private static void parseOutputForPattern(List lines, String pattern, int expected) { Pattern p = Pattern.compile(pattern); int found = 0; for (String line : lines) { Matcher m = p.matcher(line); if (m.find()) { found++; } } if (found != expected) { throw new RuntimeException("Checking for pattern \"" + pattern + "\": expected " + expected + " but found " + found); } } // straight-forward interface to JNI monitor functions static native int monitorEnter(Object o); static native int monitorExit(Object o); // Isolate the native library loading to the actual test cases, not the class that // jtreg Driver will load and execute. static class TestBase { static { System.loadLibrary("JNIMonitor"); } // This gives us a way to control the scheduler used for our virtual threads. The test // only works as intended when the virtual threads run on the same carrier thread (as // that carrier maintains ownership of the monitor if the virtual thread fails to unlock it). // The original issue was also only discovered due to the carrier thread terminating // unexpectedly, so we can force that condition too by shutting down our custom scheduler. private static Thread.Builder.OfVirtual virtualThreadBuilder(Executor scheduler) { Thread.Builder.OfVirtual builder = Thread.ofVirtual(); try { Class clazz = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder"); Constructor ctor = clazz.getDeclaredConstructor(Executor.class); ctor.setAccessible(true); return (Thread.Builder.OfVirtual) ctor.newInstance(scheduler); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof RuntimeException re) { throw re; } throw new RuntimeException(e); } catch (Exception e) { throw new RuntimeException(e); } } static void runTest(int nThreads, boolean skipUnlock, boolean throwOnExit) throws Throwable { final Object[] monitors = new Object[nThreads]; for (int i = 0; i < nThreads; i++) { monitors[i] = new Object(); } final AtomicReference exception = new AtomicReference(); // Ensure all our VT's operate of the same carrier, sequentially. ExecutorService scheduler = Executors.newSingleThreadExecutor(); ThreadFactory factory = virtualThreadBuilder(scheduler).factory(); for (int i = 0 ; i < nThreads; i++) { Object monitor = skipUnlock ? monitors[i] : monitors[0]; Thread th = factory.newThread(() -> { try { int res = monitorEnter(monitor); Asserts.assertTrue(res == 0, "monitorEnter should return 0."); Asserts.assertTrue(Thread.holdsLock(monitor), "monitor should be owned"); Thread.yield(); if (!skipUnlock) { res = monitorExit(monitor); Asserts.assertTrue(res == 0, "monitorExit should return 0."); Asserts.assertFalse(Thread.holdsLock(monitor), "monitor should be unowned"); } } catch (Throwable t) { exception.set(t); } if (throwOnExit) { throw new RuntimeException(throwMsg); } }); th.start(); th.join(); if (exception.get() != null) { throw exception.get(); } } // Now force carrier thread to shutdown. scheduler.shutdown(); } } // These are the actual test case classes that get exec'd. static class Normal extends TestBase { public static void main(String[] args) throws Throwable { runTest(1, false, false); } } static class MultiNormal extends TestBase { public static void main(String[] args) throws Throwable { runTest(MULTI_THREAD_COUNT, false, false); } } static class MissingUnlock extends TestBase { public static void main(String[] args) throws Throwable { runTest(1, true, false); } } static class MultiMissingUnlock extends TestBase { public static void main(String[] args) throws Throwable { runTest(MULTI_THREAD_COUNT, true, false); } } static class MissingUnlockWithThrow extends TestBase { public static void main(String[] args) throws Throwable { runTest(1, true, true); } } static class MultiMissingUnlockWithThrow extends TestBase { public static void main(String[] args) throws Throwable { runTest(MULTI_THREAD_COUNT, true, true); } } }