8365057: Add support for java.util.concurrent lock information to Thread.dump_to_file

Co-authored-by: Alex Menkov <amenkov@openjdk.org>
Co-authored-by: Alan Bateman <alanb@openjdk.org>
Reviewed-by: sspitsyn, alanb
This commit is contained in:
Alex Menkov 2025-09-26 20:49:36 +00:00
parent 12c0f29b97
commit cedc0117ac
6 changed files with 78 additions and 13 deletions

View File

@ -1161,9 +1161,11 @@ public:
Type _type;
// park blocker or an object the thread waiting on/trying to lock
OopHandle _obj;
// thread that owns park blocker object when park blocker is AbstractOwnableSynchronizer
OopHandle _owner;
Blocker(Type type, OopHandle obj): _type(type), _obj(obj) {}
Blocker(): _type(NOTHING), _obj(nullptr) {}
Blocker(Type type, OopHandle obj): _type(type), _obj(obj), _owner() {}
Blocker(): _type(NOTHING), _obj(), _owner() {}
bool is_empty() const {
return _type == NOTHING;
@ -1198,6 +1200,7 @@ public:
delete _locks;
}
_blocker._obj.release(oop_storage());
_blocker._owner.release(oop_storage());
}
private:
@ -1300,6 +1303,13 @@ public:
oop park_blocker = java_lang_Thread::park_blocker(_thread_h());
if (park_blocker != nullptr) {
_blocker = Blocker(Blocker::PARK_BLOCKER, OopHandle(oop_storage(), park_blocker));
if (park_blocker->is_a(vmClasses::java_util_concurrent_locks_AbstractOwnableSynchronizer_klass())) {
// could be stale (unlikely in practice), but it's good enough to see deadlocks
oop ownerObj = java_util_concurrent_locks_AbstractOwnableSynchronizer::get_owner_threadObj(park_blocker);
if (ownerObj != nullptr) {
_blocker._owner = OopHandle(oop_storage(), ownerObj);
}
}
}
ResourceMark rm(current);
@ -1381,6 +1391,7 @@ class jdk_internal_vm_ThreadSnapshot: AllStatic {
static int _locks_offset;
static int _blockerTypeOrdinal_offset;
static int _blockerObject_offset;
static int _parkBlockerOwner_offset;
static void compute_offsets(InstanceKlass* klass, TRAPS) {
JavaClasses::compute_offset(_name_offset, klass, "name", vmSymbols::string_signature(), false);
@ -1390,6 +1401,7 @@ class jdk_internal_vm_ThreadSnapshot: AllStatic {
JavaClasses::compute_offset(_locks_offset, klass, "locks", vmSymbols::jdk_internal_vm_ThreadLock_array(), false);
JavaClasses::compute_offset(_blockerTypeOrdinal_offset, klass, "blockerTypeOrdinal", vmSymbols::int_signature(), false);
JavaClasses::compute_offset(_blockerObject_offset, klass, "blockerObject", vmSymbols::object_signature(), false);
JavaClasses::compute_offset(_parkBlockerOwner_offset, klass, "parkBlockerOwner", vmSymbols::thread_signature(), false);
}
public:
static void init(InstanceKlass* klass, TRAPS) {
@ -1420,9 +1432,10 @@ public:
static void set_locks(oop snapshot, oop locks) {
snapshot->obj_field_put(_locks_offset, locks);
}
static void set_blocker(oop snapshot, int type_ordinal, oop lock) {
static void set_blocker(oop snapshot, int type_ordinal, oop lock, oop owner) {
snapshot->int_field_put(_blockerTypeOrdinal_offset, type_ordinal);
snapshot->obj_field_put(_blockerObject_offset, lock);
snapshot->obj_field_put(_parkBlockerOwner_offset, owner);
}
};
@ -1434,6 +1447,7 @@ int jdk_internal_vm_ThreadSnapshot::_stackTrace_offset;
int jdk_internal_vm_ThreadSnapshot::_locks_offset;
int jdk_internal_vm_ThreadSnapshot::_blockerTypeOrdinal_offset;
int jdk_internal_vm_ThreadSnapshot::_blockerObject_offset;
int jdk_internal_vm_ThreadSnapshot::_parkBlockerOwner_offset;
oop ThreadSnapshotFactory::get_thread_snapshot(jobject jthread, TRAPS) {
ThreadsListHandle tlh(THREAD);
@ -1559,7 +1573,8 @@ oop ThreadSnapshotFactory::get_thread_snapshot(jobject jthread, TRAPS) {
jdk_internal_vm_ThreadSnapshot::set_stack_trace(snapshot(), trace());
jdk_internal_vm_ThreadSnapshot::set_locks(snapshot(), locks());
if (!cl._blocker.is_empty()) {
jdk_internal_vm_ThreadSnapshot::set_blocker(snapshot(), cl._blocker._type, cl._blocker._obj.resolve());
jdk_internal_vm_ThreadSnapshot::set_blocker(snapshot(),
cl._blocker._type, cl._blocker._obj.resolve(), cl._blocker._owner.resolve());
}
return snapshot();
}

View File

@ -205,7 +205,10 @@ public class ThreadDumper {
// park blocker
Object parkBlocker = snapshot.parkBlocker();
if (parkBlocker != null) {
writer.println(" - parking to wait for " + decorateObject(parkBlocker));
String suffix = (snapshot.parkBlockerOwner() instanceof Thread owner)
? ", owner #" + owner.threadId()
: "";
writer.println(" - parking to wait for " + decorateObject(parkBlocker) + suffix);
}
// blocked on monitor enter or Object.wait
@ -335,6 +338,9 @@ public class ThreadDumper {
// parkBlocker is an object to allow for exclusiveOwnerThread in the future
jsonWriter.startObject("parkBlocker");
jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker));
if (snapshot.parkBlockerOwner() instanceof Thread owner) {
jsonWriter.writeProperty("owner", owner.threadId());
}
jsonWriter.endObject();
}

View File

@ -44,6 +44,8 @@ class ThreadSnapshot {
// an object the thread is blocked/waiting on, converted to ThreadBlocker by ThreadSnapshot.of()
private int blockerTypeOrdinal;
private Object blockerObject;
// the owner of the blockerObject when the object is park blocker and is AbstractOwnableSynchronizer
private Thread parkBlockerOwner;
// set by ThreadSnapshot.of()
private ThreadBlocker blocker;
@ -70,8 +72,11 @@ class ThreadSnapshot {
snapshot.locks = EMPTY_LOCKS;
}
if (snapshot.blockerObject != null) {
snapshot.blocker = new ThreadBlocker(snapshot.blockerTypeOrdinal, snapshot.blockerObject);
snapshot.blocker = new ThreadBlocker(snapshot.blockerTypeOrdinal,
snapshot.blockerObject,
snapshot.parkBlockerOwner);
snapshot.blockerObject = null; // release
snapshot.parkBlockerOwner = null;
}
return snapshot;
}
@ -104,6 +109,13 @@ class ThreadSnapshot {
return getBlocker(BlockerLockType.PARK_BLOCKER);
}
/**
* Returns the owner of the parkBlocker if the parkBlocker is an AbstractOwnableSynchronizer.
*/
Thread parkBlockerOwner() {
return (blocker != null && blocker.type == BlockerLockType.PARK_BLOCKER) ? blocker.owner : null;
}
/**
* Returns the object that the thread is blocked on.
* @throws IllegalStateException if not in the blocked state
@ -211,11 +223,11 @@ class ThreadSnapshot {
}
}
private record ThreadBlocker(BlockerLockType type, Object obj) {
private record ThreadBlocker(BlockerLockType type, Object obj, Thread owner) {
private static final BlockerLockType[] lockTypeValues = BlockerLockType.values(); // cache
ThreadBlocker(int typeOrdinal, Object obj) {
this(lockTypeValues[typeOrdinal], obj);
ThreadBlocker(int typeOrdinal, Object obj, Thread owner) {
this(lockTypeValues[typeOrdinal], obj, owner);
}
}

View File

@ -78,6 +78,10 @@
"description": "The blocker object responsible for the thread parking."
}
},
"owner": {
"type": "string",
"description": "The thread identifier of the owner when the parkBlocker is an AbstractOwnableSynchronizer."
}
"required": [
"object"
]
@ -115,9 +119,9 @@
"items": {
"type": [
"string",
null
"null"
],
"description": "The object for which the monitor is owned by the thread, null if eliminated"
"description": "The object for which the monitor is owned by the thread, null if eliminated."
}
}
},

View File

@ -23,7 +23,7 @@
/*
* @test
* @bug 8284161 8287008 8309406 8356870
* @bug 8284161 8287008 8309406 8356870 8365057
* @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads
* @requires vm.continuations
* @modules java.base/jdk.internal.vm jdk.management
@ -425,7 +425,9 @@ class DumpThreads {
ThreadFields fields = findThread(tid, lines);
assertNotNull(fields, "thread not found");
assertEquals("WAITING", fields.state());
assertTrue(contains(lines, "- parking to wait for <java.util.concurrent.locks.ReentrantLock"));
String line = find(lines, "- parking to wait for <java.util.concurrent.locks.ReentrantLock");
assertNotNull(line, "parking to wait line not found");
assertTrue(line.endsWith(" owner #" + Thread.currentThread().threadId()));
// thread dump in JSON format should include thread in root container
ThreadDump threadDump = dumpThreadsToJson();
@ -440,6 +442,12 @@ class DumpThreads {
String parkBlocker = ti.parkBlocker();
assertNotNull(parkBlocker);
assertTrue(parkBlocker.contains("java.util.concurrent.locks.ReentrantLock"));
// the owner of the parkBlocker should be the current thread
long ownerTid = ti.parkBlockerOwner().orElse(-1L);
assertNotEquals(-1L, ownerTid, "parkBlockerOwner not found");
assertEquals(Thread.currentThread().threadId(), ownerTid);
if (pinned) {
long carrierTid = ti.carrier().orElse(-1L);
assertNotEquals(-1L, carrierTid, "carrier not found");
@ -684,6 +692,16 @@ class DumpThreads {
.anyMatch(l -> l.contains(text));
}
/**
* Finds the line of a plain text thread dump containing the given text.
*/
private String find(List<String> lines, String text) {
return lines.stream().map(String::trim)
.filter(l -> l.contains(text))
.findAny()
.orElse(null);
}
/**
* Dump threads to a file in plain text format, return the lines in the file.
*/

View File

@ -296,6 +296,16 @@ public final class ThreadDump {
return getStringProperty("parkBlocker", "object");
}
/**
* Returns the owner of the parkBlocker if the parkBlocker is an AbstractOwnableSynchronizer.
*/
public OptionalLong parkBlockerOwner() {
String owner = getStringProperty("parkBlocker", "owner");
return (owner != null)
? OptionalLong.of(Long.parseLong(owner))
: OptionalLong.empty();
}
/**
* Returns the object that the thread is blocked entering its monitor.
*/