< prev index next > test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java
Print this page
/*
- * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2025, 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.
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
- import java.util.Objects;
import java.time.ZonedDateTime;
+ import java.util.List;
+ import java.util.Objects;
+ import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
+ import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
+ import java.util.concurrent.locks.ReentrantLock;
+ import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.sun.management.HotSpotDiagnosticMXBean;
import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat;
import jdk.test.lib.threaddump.ThreadDump;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.*;
+ import static org.junit.jupiter.api.Assumptions.*;
class DumpThreads {
private static boolean trackAllThreads;
@BeforeAll
static void setup() throws Exception {
String s = System.getProperty("jdk.trackAllThreads");
trackAllThreads = (s == null) || s.isEmpty() || Boolean.parseBoolean(s);
}
+ /**
+ * Test thread dump in plain text format.
+ */
+ @Test
+ void testPlainText() throws Exception {
+ List<String> lines = dumpThreadsToPlainText();
+
+ // pid should be on the first line
+ String pid = Long.toString(ProcessHandle.current().pid());
+ assertEquals(pid, lines.get(0));
+
+ // timestamp should be on the second line
+ String secondLine = lines.get(1);
+ ZonedDateTime.parse(secondLine);
+
+ // runtime version should be on third line
+ String vs = Runtime.version().toString();
+ assertEquals(vs, lines.get(2));
+
+ // dump should include current thread
+ Thread currentThread = Thread.currentThread();
+ if (trackAllThreads || !currentThread.isVirtual()) {
+ ThreadFields fields = findThread(currentThread.threadId(), lines);
+ assertNotNull(fields, "current thread not found");
+ assertEquals(currentThread.getName(), fields.name());
+ }
+ }
+
+ /**
+ * Test thread dump in JSON format.
+ */
+ @Test
+ void testJsonFormat() throws Exception {
+ ThreadDump threadDump = dumpThreadsToJson();
+
+ // dump should include current thread in the root container
+ Thread currentThread = Thread.currentThread();
+ if (trackAllThreads || !currentThread.isVirtual()) {
+ ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
+ .findThread(currentThread.threadId())
+ .orElse(null);
+ assertNotNull(ti, "current thread not found");
+ }
+ }
+
/**
* ExecutorService implementations that have their object identity in the container
* name so they can be found in the JSON format.
*/
static Stream<ExecutorService> executors() {
Executors.newVirtualThreadPerTaskExecutor()
);
}
/**
- * Test thread dump in plain text format contains information about the current
- * thread and a virtual thread created directly with the Thread API.
+ * Test that a thread container for an executor service is in the JSON format thread dump.
*/
- @Test
- void testRootContainerPlainTextFormat() throws Exception {
- Thread vthread = Thread.ofVirtual().start(LockSupport::park);
- try {
- testDumpThreadsPlainText(vthread, trackAllThreads);
- } finally {
- LockSupport.unpark(vthread);
+ @ParameterizedTest
+ @MethodSource("executors")
+ void testThreadContainer(ExecutorService executor) throws Exception {
+ try (executor) {
+ testThreadContainer(executor, Objects.toIdentityString(executor));
}
}
/**
- * Test thread dump in JSON format contains information about the current
- * thread and a virtual thread created directly with the Thread API.
+ * Test that a thread container for the common pool is in the JSON format thread dump.
*/
@Test
- void testRootContainerJsonFormat() throws Exception {
- Thread vthread = Thread.ofVirtual().start(LockSupport::park);
- try {
- testDumpThreadsJson(null, vthread, trackAllThreads);
- } finally {
- LockSupport.unpark(vthread);
- }
+ void testCommonPool() throws Exception {
+ testThreadContainer(ForkJoinPool.commonPool(), "ForkJoinPool.commonPool");
}
/**
- * Test thread dump in plain text format includes a thread executing a task in the
- * given ExecutorService.
+ * Test that the JSON thread dump has a thread container for the given executor.
*/
- @ParameterizedTest
- @MethodSource("executors")
- void testExecutorServicePlainTextFormat(ExecutorService executor) throws Exception {
- try (executor) {
- Thread thread = forkParker(executor);
- try {
- testDumpThreadsPlainText(thread, true);
- } finally {
- LockSupport.unpark(thread);
- }
+ void testThreadContainer(ExecutorService executor, String name) throws Exception {
+ var threadRef = new AtomicReference<Thread>();
+
+ executor.submit(() -> {
+ threadRef.set(Thread.currentThread());
+ LockSupport.park();
+ });
+
+ // capture Thread
+ Thread thread;
+ while ((thread = threadRef.get()) == null) {
+ Thread.sleep(20);
+ }
+
+ try {
+ // dump threads to file and parse as JSON object
+ ThreadDump threadDump = dumpThreadsToJson();
+
+ // find the thread container corresponding to the executor
+ var container = threadDump.findThreadContainer(name).orElse(null);
+ assertNotNull(container, name + " not found");
+ assertFalse(container.owner().isPresent());
+ var parent = container.parent().orElseThrow();
+ assertEquals(threadDump.rootThreadContainer(), parent);
+
+ // find the thread in the thread container
+ ThreadDump.ThreadInfo ti = container.findThread(thread.threadId()).orElse(null);
+ assertNotNull(ti, "thread not found");
+
+ } finally {
+ LockSupport.unpark(thread);
}
}
/**
- * Test thread dump in JSON format includes a thread executing a task in the
- * given ExecutorService.
+ * Test thread dump with a thread blocked on monitor enter.
*/
- @ParameterizedTest
- @MethodSource("executors")
- void testExecutorServiceJsonFormat(ExecutorService executor) throws Exception {
- try (executor) {
- Thread thread = forkParker(executor);
- try {
- testDumpThreadsJson(Objects.toIdentityString(executor), thread, true);
- } finally {
- LockSupport.unpark(thread);
+ @Test
+ void testBlockedThread() throws Exception {
+ assumeTrue(trackAllThreads, "This test requires all virtual threads to be tracked");
+ var lock = new Object();
+ var started = new CountDownLatch(1);
+
+ Thread vthread = Thread.ofVirtual().unstarted(() -> {
+ started.countDown();
+ synchronized (lock) { } // blocks
+ });
+
+ long tid = vthread.threadId();
+ String lockAsString = Objects.toIdentityString(lock);
+
+ try {
+ synchronized (lock) {
+ // start thread and wait for it to block
+ vthread.start();
+ started.await();
+ await(vthread, Thread.State.BLOCKED);
+
+ // thread dump in plain text should include thread
+ List<String> lines = dumpThreadsToPlainText();
+ ThreadFields fields = findThread(tid, lines);
+ assertNotNull(fields, "thread not found");
+ assertEquals("BLOCKED", fields.state());
+ assertTrue(contains(lines, "// blocked on " + lockAsString));
+
+ // thread dump in JSON format should include thread in root container
+ ThreadDump threadDump = dumpThreadsToJson();
+ ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
+ .findThread(tid)
+ .orElse(null);
+ assertNotNull(ti, "thread not found");
+ assertEquals("BLOCKED", ti.state());
+ assertEquals(lockAsString, ti.blockedOn());
}
+ } finally {
+ vthread.join();
}
}
/**
- * Test thread dump in JSON format includes a thread executing a task in the
- * fork-join common pool.
+ * Test thread dump with a thread waiting in Object.wait.
*/
@Test
- void testForkJoinPool() throws Exception {
- ForkJoinPool pool = ForkJoinPool.commonPool();
- Thread thread = forkParker(pool);
+ void testWaitingThread() throws Exception {
+ assumeTrue(trackAllThreads, "This test requires all virtual threads to be tracked");
+ var lock = new Object();
+ var started = new CountDownLatch(1);
+
+ Thread vthread = Thread.ofVirtual().start(() -> {
+ synchronized (lock) {
+ started.countDown();
+ try {
+ lock.wait();
+ } catch (InterruptedException e) { }
+ }
+ });
+
+ long tid = vthread.threadId();
+ String lockAsString = Objects.toIdentityString(lock);
+
try {
- testDumpThreadsJson("ForkJoinPool.commonPool", thread, true);
+ // wait for thread to be waiting in Object.wait
+ started.await();
+ await(vthread, Thread.State.WAITING);
+
+ // thread dump in plain text should include thread
+ List<String> lines = dumpThreadsToPlainText();
+ ThreadFields fields = findThread(tid, lines);
+ assertNotNull(fields, "thread not found");
+ assertEquals("WAITING", fields.state());
+ assertTrue(contains(lines, "// waiting on " + lockAsString));
+
+ // thread dump in JSON format should include thread in root container
+ ThreadDump threadDump = dumpThreadsToJson();
+ ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
+ .findThread(vthread.threadId())
+ .orElse(null);
+ assertNotNull(ti, "thread not found");
+ assertEquals("WAITING", ti.state());
+ assertEquals(Objects.toIdentityString(lock), ti.waitingOn());
+
} finally {
- LockSupport.unpark(thread);
+ synchronized (lock) {
+ lock.notifyAll();
+ }
+ vthread.join();
}
}
/**
- * Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in plain text
- * format, then sanity check that the thread dump includes expected strings, the
- * current thread, and maybe the given thread.
- * @param thread the thread to test if included
- * @param expectInDump true if the thread is expected to be included
+ * Test thread dump with a thread parked on a j.u.c. lock.
*/
- private void testDumpThreadsPlainText(Thread thread, boolean expectInDump) throws Exception {
- Path file = genOutputPath(".txt");
- var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
- mbean.dumpThreads(file.toString(), ThreadDumpFormat.TEXT_PLAIN);
- System.err.format("Dumped to %s%n", file);
-
- // pid should be on the first line
- String line1 = line(file, 0);
- String pid = Long.toString(ProcessHandle.current().pid());
- assertTrue(line1.contains(pid));
-
- // timestamp should be on the second line
- String line2 = line(file, 1);
- ZonedDateTime.parse(line2);
-
- // runtime version should be on third line
- String line3 = line(file, 2);
- String vs = Runtime.version().toString();
- assertTrue(line3.contains(vs));
+ @Test
+ void testParkedThread() throws Exception {
+ assumeTrue(trackAllThreads, "This test requires all virtual threads to be tracked");
+ var lock = new ReentrantLock();
+ var started = new CountDownLatch(1);
+
+ Thread vthread = Thread.ofVirtual().unstarted(() -> {
+ started.countDown();
+ lock.lock();
+ lock.unlock();
+ });
- // test if thread is included in thread dump
- assertEquals(expectInDump, isPresent(file, thread));
+ long tid = vthread.threadId();
- // current thread should be included if platform thread or tracking all threads
- Thread currentThread = Thread.currentThread();
- boolean currentThreadExpected = trackAllThreads || !currentThread.isVirtual();
- assertEquals(currentThreadExpected, isPresent(file, currentThread));
+ lock.lock();
+ try {
+ // start thread and wait for it to park
+ vthread.start();
+ started.await();
+ await(vthread, Thread.State.WAITING);
+
+ // thread dump in plain text should include thread
+ List<String> lines = dumpThreadsToPlainText();
+ ThreadFields fields = findThread(tid, lines);
+ assertNotNull(fields, "thread not found");
+ assertEquals("WAITING", fields.state());
+ assertTrue(contains(lines, "// parked on java.util.concurrent.locks.ReentrantLock"));
+
+ // thread dump in JSON format should include thread in root container
+ ThreadDump threadDump = dumpThreadsToJson();
+ ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
+ .findThread(vthread.threadId())
+ .orElse(null);
+ assertNotNull(ti, "thread not found");
+ assertEquals("WAITING", ti.state());
+ String parkBlocker = ti.parkBlocker();
+ assertNotNull(parkBlocker);
+ assertTrue(parkBlocker.contains("java.util.concurrent.locks.ReentrantLock"));
+ } finally {
+ lock.unlock();
+ }
}
/**
- * Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in JSON format.
- * The thread dump is parsed as a JSON object and checked to ensure that it contains
- * expected data, the current thread, and maybe the given thread.
- * @param containerName the name of the container or null for the root container
- * @param thread the thread to test if included
- * @param expect true if the thread is expected to be included
+ * Test thread dump wth a thread owning a monitor.
*/
- private void testDumpThreadsJson(String containerName,
- Thread thread,
- boolean expectInDump) throws Exception {
- Path file = genOutputPath(".json");
- var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
- mbean.dumpThreads(file.toString(), ThreadDumpFormat.JSON);
- System.err.format("Dumped to %s%n", file);
+ @Test
+ void testThreadOwnsMonitor() throws Exception {
+ assumeTrue(trackAllThreads, "This test requires all virtual threads to be tracked");
+ var lock = new Object();
+ var started = new CountDownLatch(1);
+
+ Thread vthread = Thread.ofVirtual().start(() -> {
+ synchronized (lock) {
+ started.countDown();
+ LockSupport.park();
+ }
+ });
- // parse the JSON text
- String jsonText = Files.readString(file);
- ThreadDump threadDump = ThreadDump.parse(jsonText);
-
- // test threadDump/processId
- assertTrue(threadDump.processId() == ProcessHandle.current().pid());
-
- // test threadDump/time can be parsed
- ZonedDateTime.parse(threadDump.time());
-
- // test threadDump/runtimeVersion
- assertEquals(Runtime.version().toString(), threadDump.runtimeVersion());
-
- // test root container, has no parent and no owner
- var rootContainer = threadDump.rootThreadContainer();
- assertFalse(rootContainer.owner().isPresent());
- assertFalse(rootContainer.parent().isPresent());
-
- // test that the container contains the given thread
- ThreadDump.ThreadContainer container;
- if (containerName == null) {
- // root container, the thread should be found if trackAllThreads is true
- container = rootContainer;
- } else {
- // find the container
- container = threadDump.findThreadContainer(containerName).orElse(null);
- assertNotNull(container, containerName + " not found");
- assertFalse(container.owner().isPresent());
- assertTrue(container.parent().get() == rootContainer);
+ long tid = vthread.threadId();
+ String lockAsString = Objects.toIdentityString(lock);
+ try {
+ // wait for thread to park
+ started.await();
+ await(vthread, Thread.State.WAITING);
+
+ // thread dump in plain text should include thread
+ List<String> lines = dumpThreadsToPlainText();
+ ThreadFields fields = findThread(tid, lines);
+ assertNotNull(fields, "thread not found");
+ assertEquals("WAITING", fields.state());
+ assertTrue(contains(lines, "// locked " + lockAsString));
+
+ // thread dump in JSON format should include thread in root container
+ ThreadDump threadDump = dumpThreadsToJson();
+ ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
+ .findThread(tid)
+ .orElse(null);
+ assertNotNull(ti, "thread not found");
+ // the lock should be in the ownedMonitors array
+ Set<String> ownedMonitors = ti.ownedMonitors().values()
+ .stream()
+ .flatMap(List::stream)
+ .collect(Collectors.toSet());
+ assertTrue(ownedMonitors.contains(lockAsString), lockAsString + " not found");
+ } finally {
+ LockSupport.unpark(vthread);
}
- boolean found = container.findThread(thread.threadId()).isPresent();
- assertEquals(expectInDump, found);
-
- // current thread should be in root container if platform thread or tracking all threads
- Thread currentThread = Thread.currentThread();
- boolean currentThreadExpected = trackAllThreads || !currentThread.isVirtual();
- found = rootContainer.findThread(currentThread.threadId()).isPresent();
- assertEquals(currentThreadExpected, found);
}
/**
* Test that dumpThreads throws if the output file already exists.
*/
assertThrows(NullPointerException.class,
() -> mbean.dumpThreads(genOutputPath("txt").toString(), null));
}
/**
- * Submits a parking task to the given executor, returns the Thread object of
- * the parked thread.
+ * Represents the data for a thread found in a plain text thread dump.
*/
- private static Thread forkParker(ExecutorService executor) {
- class Box { static volatile Thread thread;}
- var latch = new CountDownLatch(1);
- executor.submit(() -> {
- Box.thread = Thread.currentThread();
- latch.countDown();
- LockSupport.park();
- });
- try {
- latch.await();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
+ private record ThreadFields(long tid, String name, String state) { }
+
+ /**
+ * Find a thread in the lines of a plain text thread dump.
+ */
+ private ThreadFields findThread(long tid, List<String> lines) {
+ String line = lines.stream()
+ .filter(l -> l.startsWith("#" + tid + " "))
+ .findFirst()
+ .orElse(null);
+ if (line == null) {
+ return null;
}
- return Box.thread;
+
+ // #3 "main" RUNNABLE 2025-04-18T15:22:12.012450Z
+ String[] components = line.split("\\s+"); // assume no spaces in thread name
+ assertEquals(4, components.length);
+ String nameInQuotes = components[1];
+ String name = nameInQuotes.substring(1, nameInQuotes.length()-1);
+ String state = components[2];
+ return new ThreadFields(tid, name, state);
+ }
+
+ /**
+ * Returns true if lines of a plain text thread dump contain the given text.
+ */
+ private boolean contains(List<String> lines, String text) {
+ return lines.stream().map(String::trim)
+ .anyMatch(l -> l.contains(text));
}
/**
- * Returns true if a Thread is present in a plain text thread dump.
+ * Dump threads to a file in plain text format, return the lines in the file.
*/
- private static boolean isPresent(Path file, Thread thread) throws Exception {
- String expect = "#" + thread.threadId();
- return count(file, expect) > 0;
+ private List<String> dumpThreadsToPlainText() throws Exception {
+ Path file = genOutputPath(".txt");
+ var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
+ mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.TEXT_PLAIN);
+ System.err.format("Dumped to %s%n", file);
+ return Files.readAllLines(file);
+ }
+
+ /**
+ * Dump threads to a file in JSON format, parse and return as JSON object.
+ */
+ private static ThreadDump dumpThreadsToJson() throws Exception {
+ Path file = genOutputPath(".json");
+ var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
+ mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON);
+ System.err.format("Dumped to %s%n", file);
+ String jsonText = Files.readString(file);
+ return ThreadDump.parse(jsonText);
}
/**
* Generate a file path with the given suffix to use as an output file.
*/
Files.delete(file);
return file;
}
/**
- * Return the count of the number of files in the given file that contain
- * the given character sequence.
- */
- static long count(Path file, CharSequence cs) throws Exception {
- try (Stream<String> stream = Files.lines(file)) {
- return stream.filter(line -> line.contains(cs)).count();
- }
- }
-
- /**
- * Return line $n of the given file.
+ * Waits for the given thread to get to a given state.
*/
- private String line(Path file, long n) throws Exception {
- try (Stream<String> stream = Files.lines(file)) {
- return stream.skip(n).findFirst().orElseThrow();
+ private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
+ Thread.State state = thread.getState();
+ while (state != expectedState) {
+ assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
+ Thread.sleep(10);
+ state = thread.getState();
}
}
}
< prev index next >