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 "JNIMonitor$" + test, 123 }; 124 OutputAnalyzer oa = ProcessTools.executeTestJava(cmdArgs); 125 oa.shouldHaveExitValue(0); 126 oa.stdoutShouldMatch(terminated); 127 128 switch(test) { 129 case "Normal": 130 case "MultiNormal": 131 oa.stdoutShouldNotMatch(stillLocked); 132 break; 133 case "MissingUnlock": 134 oa.stdoutShouldMatch(stillLocked); 135 break; 136 case "MultiMissingUnlock": 137 parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT); 138 break; 139 case "MissingUnlockWithThrow": 140 oa.stdoutShouldMatch(stillLocked); 141 oa.stderrShouldContain(throwMsg); 142 break; 143 case "MultiMissingUnlockWithThrow": 144 parseOutputForPattern(oa.stdoutAsLines(), stillLocked, MULTI_THREAD_COUNT); 145 parseOutputForPattern(oa.stderrAsLines(), throwMsg, MULTI_THREAD_COUNT); 146 break; 147 148 default: throw new Error("Unknown arg: " + args[0]); 149 } 150 oa.reportDiagnosticSummary(); 151 } 152 153 // The number of threads for a multi tests. Arbitrarily chosen to be > 1 but small 154 // enough to not waste too much time. 155 static final int MULTI_THREAD_COUNT = 5; 156 157 // The logging message for leaving a monitor JNI locked has the form 158 // [0.187s][debug][jni] VirtualThread (tid: 28, carrier id: 29) exiting with Objects still locked by JNI MonitorEnter. 159 // but if the test is run with other logging options then whitespace may get introduced in the 160 // log decorator sections, so ignore those. 161 static final String stillLocked = "VirtualThread \\(tid:.*exiting with Objects still locked by JNI MonitorEnter"; 162 // The carrier thread termination logging has the form: 163 // [1.394s][info][os,thread] JavaThread exiting (name: "pool-1-thread-1", tid: 3090592). 164 static final String terminated = "JavaThread exiting \\(name: \"pool-1-thread-1\""; 165 166 static final String throwMsg = "Terminating via exception as requested"; 167 168 // Check the process logging output for the given pattern to see if the expected number of 169 // lines are found. 170 private static void parseOutputForPattern(List<String> lines, String pattern, int expected) { 171 Pattern p = Pattern.compile(pattern); 172 int found = 0; 173 for (String line : lines) { 174 Matcher m = p.matcher(line); 175 if (m.find()) { 176 found++; 177 } 178 } 179 if (found != expected) { 180 throw new RuntimeException("Checking for pattern \"" + pattern + "\": expected " 181 + expected + " but found " + found); 182 } 183 } 184 185 186 // straight-forward interface to JNI monitor functions 187 static native int monitorEnter(Object o); 188 static native int monitorExit(Object o); 189 190 // Isolate the native library loading to the actual test cases, not the class that 191 // jtreg Driver will load and execute. 192 static class TestBase { 193 194 static { 195 System.loadLibrary("JNIMonitor"); 196 } 197 198 // This gives us a way to control the scheduler used for our virtual threads. The test 199 // only works as intended when the virtual threads run on the same carrier thread (as 200 // that carrier maintains ownership of the monitor if the virtual thread fails to unlock it). 201 // The original issue was also only discovered due to the carrier thread terminating 202 // unexpectedly, so we can force that condition too by shutting down our custom scheduler. 203 private static Thread.Builder.OfVirtual virtualThreadBuilder(Executor scheduler) { 204 Thread.Builder.OfVirtual builder = Thread.ofVirtual(); 205 try { 206 Class<?> clazz = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder"); 207 Constructor<?> ctor = clazz.getDeclaredConstructor(Executor.class); 208 ctor.setAccessible(true); 209 return (Thread.Builder.OfVirtual) ctor.newInstance(scheduler); 210 } catch (InvocationTargetException e) { 211 Throwable cause = e.getCause(); 212 if (cause instanceof RuntimeException re) { 213 throw re; 214 } 215 throw new RuntimeException(e); 216 } catch (Exception e) { 217 throw new RuntimeException(e); 218 } 219 } 220 221 static void runTest(int nThreads, boolean skipUnlock, boolean throwOnExit) throws Throwable { 222 final Object monitor = new Object(); 223 final AtomicReference<Throwable> exception = new AtomicReference(); 224 // Ensure all our VT's operate of the same carrier, sequentially. 225 ExecutorService scheduler = Executors.newSingleThreadExecutor(); 226 ThreadFactory factory = virtualThreadBuilder(scheduler).factory(); 227 for (int i = 0 ; i < nThreads; i++) { 228 Thread th = factory.newThread(() -> { 229 try { 230 int res = monitorEnter(monitor); 231 Asserts.assertTrue(res == 0, "monitorEnter should return 0."); 232 Asserts.assertTrue(Thread.holdsLock(monitor), "monitor should be owned"); 233 Thread.yield(); 234 if (!skipUnlock) { 235 res = monitorExit(monitor); 236 Asserts.assertTrue(res == 0, "monitorExit should return 0."); 237 Asserts.assertFalse(Thread.holdsLock(monitor), "monitor should be unowned"); 238 } 239 } catch (Throwable t) { 240 exception.set(t); 241 } 242 if (throwOnExit) { 243 throw new RuntimeException(throwMsg); 244 } 245 }); 246 th.start(); 247 th.join(); 248 if (exception.get() != null) { 249 throw exception.get(); 250 } 251 } 252 // Now force carrier thread to shutdown. 253 scheduler.shutdown(); 254 } 255 } 256 257 // These are the actual test case classes that get exec'd. 258 259 static class Normal extends TestBase { 260 public static void main(String[] args) throws Throwable { 261 runTest(1, false, false); 262 } 263 } 264 265 static class MultiNormal extends TestBase { 266 public static void main(String[] args) throws Throwable { 267 runTest(MULTI_THREAD_COUNT, false, false); 268 } 269 } 270 271 static class MissingUnlock extends TestBase { 272 public static void main(String[] args) throws Throwable { 273 runTest(1, true, false); 274 } 275 } 276 277 static class MultiMissingUnlock extends TestBase { 278 public static void main(String[] args) throws Throwable { 279 runTest(MULTI_THREAD_COUNT, true, false); 280 } 281 } 282 283 static class MissingUnlockWithThrow extends TestBase { 284 public static void main(String[] args) throws Throwable { 285 runTest(1, true, true); 286 } 287 } 288 289 static class MultiMissingUnlockWithThrow extends TestBase { 290 public static void main(String[] args) throws Throwable { 291 runTest(MULTI_THREAD_COUNT, true, true); 292 } 293 } 294 295 }