1 /* 2 * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 import jdk.test.lib.Asserts; 24 import jdk.test.lib.Utils; 25 import jdk.test.lib.process.ProcessTools; 26 import jdk.test.lib.process.OutputAnalyzer; 27 28 import java.lang.reflect.Constructor; 29 import java.lang.reflect.InvocationTargetException; 30 import java.util.List; 31 import java.util.concurrent.atomic.AtomicReference; 32 import java.util.concurrent.ExecutorService; 33 import java.util.concurrent.Executor; 34 import java.util.concurrent.Executors; 35 import java.util.concurrent.ThreadFactory; 36 import java.util.regex.Matcher; 37 import java.util.regex.Pattern; 38 39 /* 40 * Tests that JNI monitors work correctly with virtual threads, 41 * There are multiple test scenarios that we check using unified logging output 42 * (both positive and negative tests). Each test case is handled by its own @-test 43 * definition so that we can run each sub-test independently. 44 * 45 * The original bug was only discovered because the ForkJoinPool worker thread terminated 46 * and trigerred an assertion failure. So we use a custom scheduler to give us control. 47 */ 48 49 /** 50 * @test id=normal 51 * @bug 8327743 52 * @summary Normal lock then unlock 53 * @library /test/lib 54 * @modules java.base/java.lang:+open 55 * @requires vm.continuations 56 * @run driver JNIMonitor Normal 57 */ 58 59 /** 60 * @test id=multiNormal 61 * @bug 8327743 62 * @summary Normal lock then unlock by multiple threads 63 * @library /test/lib 64 * @modules java.base/java.lang:+open 65 * @requires vm.continuations 66 * @run driver JNIMonitor MultiNormal 67 */ 68 69 /** 70 * @test id=missingUnlock 71 * @bug 8327743 72 * @summary Don't do the unlock and exit normally 73 * @library /test/lib 74 * @modules java.base/java.lang:+open 75 * @requires vm.continuations 76 * @run driver JNIMonitor MissingUnlock 77 */ 78 79 /** 80 * @test id=multiMissingUnlock 81 * @bug 8327743 82 * @summary Don't do the unlock and exit normally, by multiple threads 83 * @library /test/lib 84 * @modules java.base/java.lang:+open 85 * @requires vm.continuations 86 * @run driver JNIMonitor MultiMissingUnlock 87 */ 88 89 /** 90 * @test id=missingUnlockWithThrow 91 * @bug 8327743 92 * @summary Don't do the unlock and exit by throwing 93 * @library /test/lib 94 * @modules java.base/java.lang:+open 95 * @requires vm.continuations 96 * @run driver JNIMonitor MissingUnlockWithThrow 97 */ 98 99 /** 100 * @test id=multiMissingUnlockWithThrow 101 * @bug 8327743 102 * @summary Don't do the unlock and exit by throwing, by multiple threads 103 * @library /test/lib 104 * @modules java.base/java.lang:+open 105 * @requires vm.continuations 106 * @run driver JNIMonitor MultiMissingUnlockWithThrow 107 */ 108 109 public class JNIMonitor { 110 111 public static void main(String[] args) throws Exception { 112 String test = args[0]; 113 String[] cmdArgs = new String[] { 114 "-Djava.library.path=" + Utils.TEST_NATIVE_PATH, 115 // Grant access to ThreadBuilders$VirtualThreadBuilder 116 "--add-opens=java.base/java.lang=ALL-UNNAMED", 117 // Enable the JNI warning 118 "-Xcheck:jni", 119 "-Xlog:jni=debug", 120 // Enable thread termination logging as a visual cross-check 121 "-Xlog:thread+os=info", 122 // We only count monitors in LM_LEGACY mode 123 "-XX:LockingMode=1", 124 // Disable compact headers since that switches locking mode to LM_LIGHTWEIGHT 125 "-XX:-UseCompactObjectHeaders", 126 "JNIMonitor$" + test, 127 }; 128 OutputAnalyzer oa = ProcessTools.executeTestJava(cmdArgs); 129 oa.shouldHaveExitValue(0); 130 oa.stdoutShouldMatch(terminated); 131 132 switch(test) { 133 case "Normal": 134 case "MultiNormal": 135 oa.stdoutShouldNotMatch(stillLocked); 136 break; 137 case "MissingUnlock": 138 oa.stdoutShouldMatch(stillLocked); 139 break; 140 case "MultiMissingUnlock": 141 parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT); 142 break; 143 case "MissingUnlockWithThrow": 144 oa.stdoutShouldMatch(stillLocked); 145 oa.stderrShouldContain(throwMsg); 146 break; 147 case "MultiMissingUnlockWithThrow": 148 parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT); 149 parseOutputForPattern(oa.stderrAsLines(), throwMsg, MULTI_THREAD_COUNT); 150 break; 151 152 default: throw new Error("Unknown arg: " + args[0]); 153 } 154 oa.reportDiagnosticSummary(); 155 } 156 157 // The number of threads for a multi tests. Arbitrarily chosen to be > 1 but small 158 // enough to not waste too much time. 159 static final int MULTI_THREAD_COUNT = 5; 160 161 // The logging message for leaving a monitor JNI locked has the form 162 // [0.187s][debug][jni] VirtualThread (tid: 28, carrier id: 29) exiting with Objects still locked by JNI MonitorEnter. 163 // but if the test is run with other logging options then whitespace may get introduced in the 164 // log decorator sections, so ignore those. 165 static final String stillLocked = "VirtualThread \\(tid:.*exiting with Objects still locked by JNI MonitorEnter"; 166 // The carrier thread termination logging has the form: 167 // [1.394s][info][os,thread] JavaThread exiting (name: "pool-1-thread-1", tid: 3090592). 168 static final String terminated = "JavaThread exiting \\(name: \"pool-1-thread-1\""; 169 170 static final String throwMsg = "Terminating via exception as requested"; 171 172 // Check the process logging output for the given pattern to see if the expected number of 173 // lines are found. 174 private static void parseOutputForPattern(List<String> lines, String pattern, int expected) { 175 Pattern p = Pattern.compile(pattern); 176 int found = 0; 177 for (String line : lines) { 178 Matcher m = p.matcher(line); 179 if (m.find()) { 180 found++; 181 } 182 } 183 if (found != expected) { 184 throw new RuntimeException("Checking for pattern \"" + pattern + "\": expected " 185 + expected + " but found " + found); 186 } 187 } 188 189 190 // straight-forward interface to JNI monitor functions 191 static native int monitorEnter(Object o); 192 static native int monitorExit(Object o); 193 194 // Isolate the native library loading to the actual test cases, not the class that 195 // jtreg Driver will load and execute. 196 static class TestBase { 197 198 static { 199 System.loadLibrary("JNIMonitor"); 200 } 201 202 // This gives us a way to control the scheduler used for our virtual threads. The test 203 // only works as intended when the virtual threads run on the same carrier thread (as 204 // that carrier maintains ownership of the monitor if the virtual thread fails to unlock it). 205 // The original issue was also only discovered due to the carrier thread terminating 206 // unexpectedly, so we can force that condition too by shutting down our custom scheduler. 207 private static Thread.Builder.OfVirtual virtualThreadBuilder(Executor scheduler) { 208 Thread.Builder.OfVirtual builder = Thread.ofVirtual(); 209 try { 210 Class<?> clazz = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder"); 211 Constructor<?> ctor = clazz.getDeclaredConstructor(Executor.class); 212 ctor.setAccessible(true); 213 return (Thread.Builder.OfVirtual) ctor.newInstance(scheduler); 214 } catch (InvocationTargetException e) { 215 Throwable cause = e.getCause(); 216 if (cause instanceof RuntimeException re) { 217 throw re; 218 } 219 throw new RuntimeException(e); 220 } catch (Exception e) { 221 throw new RuntimeException(e); 222 } 223 } 224 225 static void runTest(int nThreads, boolean skipUnlock, boolean throwOnExit) throws Throwable { 226 final Object[] monitors = new Object[nThreads]; 227 for (int i = 0; i < nThreads; i++) { 228 monitors[i] = new Object(); 229 } 230 final AtomicReference<Throwable> exception = new AtomicReference(); 231 // Ensure all our VT's operate of the same carrier, sequentially. 232 ExecutorService scheduler = Executors.newSingleThreadExecutor(); 233 ThreadFactory factory = virtualThreadBuilder(scheduler).factory(); 234 for (int i = 0 ; i < nThreads; i++) { 235 Object monitor = skipUnlock ? monitors[i] : monitors[0]; 236 Thread th = factory.newThread(() -> { 237 try { 238 int res = monitorEnter(monitor); 239 Asserts.assertTrue(res == 0, "monitorEnter should return 0."); 240 Asserts.assertTrue(Thread.holdsLock(monitor), "monitor should be owned"); 241 Thread.yield(); 242 if (!skipUnlock) { 243 res = monitorExit(monitor); 244 Asserts.assertTrue(res == 0, "monitorExit should return 0."); 245 Asserts.assertFalse(Thread.holdsLock(monitor), "monitor should be unowned"); 246 } 247 } catch (Throwable t) { 248 exception.set(t); 249 } 250 if (throwOnExit) { 251 throw new RuntimeException(throwMsg); 252 } 253 }); 254 th.start(); 255 th.join(); 256 if (exception.get() != null) { 257 throw exception.get(); 258 } 259 } 260 // Now force carrier thread to shutdown. 261 scheduler.shutdown(); 262 } 263 } 264 265 // These are the actual test case classes that get exec'd. 266 267 static class Normal extends TestBase { 268 public static void main(String[] args) throws Throwable { 269 runTest(1, false, false); 270 } 271 } 272 273 static class MultiNormal extends TestBase { 274 public static void main(String[] args) throws Throwable { 275 runTest(MULTI_THREAD_COUNT, false, false); 276 } 277 } 278 279 static class MissingUnlock extends TestBase { 280 public static void main(String[] args) throws Throwable { 281 runTest(1, true, false); 282 } 283 } 284 285 static class MultiMissingUnlock extends TestBase { 286 public static void main(String[] args) throws Throwable { 287 runTest(MULTI_THREAD_COUNT, true, false); 288 } 289 } 290 291 static class MissingUnlockWithThrow extends TestBase { 292 public static void main(String[] args) throws Throwable { 293 runTest(1, true, true); 294 } 295 } 296 297 static class MultiMissingUnlockWithThrow extends TestBase { 298 public static void main(String[] args) throws Throwable { 299 runTest(MULTI_THREAD_COUNT, true, true); 300 } 301 } 302 303 }