From c1c98f715662208bc9fe69f5e94276d6983779bd Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:41:26 -0600 Subject: [PATCH] Implement ThreadMXBean thread info, deadlock detection and locked monitor and synchronizer dumps --- docs/support.md | 7 +- src/jvm.ts | 3 + src/natives/lang.ts | 4 + src/natives/management.ts | 375 ++++++++++++++++++++++++++++++++++++- src/snapshot.ts | 2 + src/thread.ts | 6 + test/java/ThreadInfos.java | 226 ++++++++++++++++++++++ test/jtreg/passing.txt | 11 ++ test/jtreg/times.tsv | 11 ++ 9 files changed, 636 insertions(+), 9 deletions(-) create mode 100644 test/java/ThreadInfos.java diff --git a/docs/support.md b/docs/support.md index 54ed54b..21fc4d4 100644 --- a/docs/support.md +++ b/docs/support.md @@ -30,9 +30,12 @@ java-web targets headless Java: console apps, libraries and javac. for, less the process's umask. In-memory files get synthetic inode numbers, a link count of 1 and default modes. - `-XX:OnOutOfMemoryError` runs no command, and `StackWalker` - has no live locals or operands. `ThreadMXBean.getThreadInfo` and - `dumpThreads` are not implemented, and a virtual thread that yields while it + has no live locals or operands. A virtual thread that yields while it holds a monitor stays pinned to its carrier. +- `ThreadMXBean` describes threads and finds deadlocks, but has no CPU times + and no blocked or waited times (contention monitoring is off). Thread dumps + find a frame's locked monitors from its synchronized blocks as compilers emit + them, and list a monitor taken any other way with stack depth -1. - `WatchService` reports files the program itself, or another thread in the same VM, creates, changes or deletes. It does not see changes made outside the VM, including by child processes the program starts. diff --git a/src/jvm.ts b/src/jvm.ts index cab9675..d6ccc17 100644 --- a/src/jvm.ts +++ b/src/jvm.ts @@ -902,6 +902,7 @@ export class JVM { if (this.tryMonitorEnter(t, o)) { if (t.blockedOnMonitor) { t.blockedOnMonitor = false; + t.pendingOn = null; this.setThreadStatus(t, STATUS_RUNNABLE); } return true; @@ -909,6 +910,8 @@ export class JVM { // Thread.getState() says BLOCKED while it waits, as on HotSpot if (!t.blockedOnMonitor) { t.blockedOnMonitor = true; + t.blockedCount++; + t.pendingOn = o; this.setThreadStatus(t, STATUS_BLOCKED); } t.state = WAITING; diff --git a/src/natives/lang.ts b/src/natives/lang.ts index c5c51d1..bafd353 100644 --- a/src/natives/lang.ts +++ b/src/natives/lang.ts @@ -58,6 +58,8 @@ function* waitImpl(t: JThread, self: JObject, ms: bigint): NativeGen { const count = mon.count; vm.releaseMonitor(mon); setThreadStatus(t, ms > 0n ? STATUS_TIMED_WAITING : STATUS_WAITING); + t.waitingOn = self; + t.waitedCount++; let entry: { t: JThread; wake: () => void; count: number } | null = null; yield t.block(interruptibleWait(t, ms > 0n ? Number(ms) : null, (wake) => { entry = { t, wake, count }; @@ -69,6 +71,7 @@ function* waitImpl(t: JThread, self: JObject, ms: bigint): NativeGen { while (mon.owner !== null) yield t.block(new Promise((r) => mon.entry.push(r))); mon.owner = t; mon.count = count; + t.waitingOn = null; setThreadStatus(t, STATUS_RUNNABLE); checkInterrupt(t); } @@ -242,6 +245,7 @@ export const natives: Natives = { "java/lang/Thread.sleepNanos0(J)V": function* (t, [nanos]): NativeGen { checkInterrupt(t); setThreadStatus(t, STATUS_SLEEPING); + t.waitedCount++; // timers can fire a little early by the clock nanoTime reads; sleep at least as asked const end = performance.now() + Number(nanos) / 1e6; try { diff --git a/src/natives/management.ts b/src/natives/management.ts index 1d0bdff..e62960d 100644 --- a/src/natives/management.ts +++ b/src/natives/management.ts @@ -5,13 +5,18 @@ // heap's; collections are the explicit System.gc() calls, which HotSpot counts // as full collections. -import { MAX_HEAP } from "../runtime.ts"; -import type { JMethod, JObject } from "../runtime.ts"; -import { TERMINATED } from "../thread.ts"; -import type { JThread, NativeGen } from "../thread.ts"; +import { INITIALIZED, JArray, JClass, JObject, MAX_HEAP } from "../runtime.ts"; +import type { JMethod } from "../runtime.ts"; +import { + Frame, JThread, NativeFrame, PendingThrow, STATUS_BLOCKED, STATUS_NEW, STATUS_PARKED, STATUS_PARKED_TIMED, STATUS_RUNNABLE, + STATUS_TERMINATED, STATUS_TIMED_WAITING, STATUS_WAITING, TERMINATED, +} from "../thread.ts"; +import type { NativeGen } from "../thread.ts"; import type { JVM } from "../jvm.ts"; import { openFdCount } from "./io.ts"; import { box, initialized } from "./reflect.ts"; +import { continuationEntry } from "./continuation.ts"; +import { fillElement } from "./lang.ts"; import type { Natives } from "./util.ts"; const MANAGERS = [ @@ -143,6 +148,341 @@ const FLAGS: { name: string; value: boolean | number; writeable: boolean }[] = [ { name: "UseSerialGC", value: true, writeable: false }, ]; +const TH = "java/lang/Thread"; +const AOS = "java/util/concurrent/locks/AbstractOwnableSynchronizer"; +const THREAD_INFO = "java/lang/management/ThreadInfo"; +const THREAD_INFO_INIT = "(Ljava/lang/Thread;ILjava/lang/Object;Ljava/lang/Thread;JJJJ[Ljava/lang/StackTraceElement;)V"; +const THREAD_INFO_LOCKS_INIT = + "(Ljava/lang/Thread;ILjava/lang/Object;Ljava/lang/Thread;JJJJ[Ljava/lang/StackTraceElement;[Ljava/lang/Object;[I[Ljava/lang/Object;)V"; + +function threadStatus(vm: JVM, t: JThread): number { + const holder = vm.getField(t.obj!, TH, "holder"); + return holder ? vm.getField(holder, "java/lang/Thread$FieldHolder", "threadStatus") : STATUS_NEW; +} + +// The live platform thread with this id, as HotSpot's thread list has it. +function threadById(vm: JVM, tid: bigint): JThread | null { + for (const t of liveThreads(vm)) { + if (vm.getField(t.obj!, TH, "tid") === tid && threadStatus(vm, t) !== STATUS_TERMINATED) return t; + } + return null; +} + +// A carrier thread with a virtual thread mounted +function isCarrier(t: JThread): boolean { + return t.mounted !== null && t.mounted !== t.obj; +} + +function isSynchronizer(vm: JVM, o: JObject | null): boolean { + return o !== null && o.cls.isAssignableTo(vm.loadBoot(AOS)!); +} + +function synchronizerOwner(vm: JVM, o: JObject): JObject | null { + return vm.getField(o, AOS, "exclusiveOwnerThread"); +} + +function platformThread(o: JObject | null): JThread | null { + const x = o?.x; + return x instanceof JThread && x.state !== TERMINATED ? x : null; +} + +function parkBlocker(vm: JVM, t: JThread): JObject | null { + return vm.getField(t.obj!, TH, "parkBlocker"); +} + +interface Shown { + m: JMethod; + pc: number; + // index in the thread's frame stack + at: number; +} + +// The Java frames HotSpot shows for a thread, top first: all of them, hidden +// ones too. A carrier thread shows its own frames, below its virtual thread's. +function threadFrames(t: JThread, maxDepth: number): Shown[] { + const out: Shown[] = []; + let i = t.frames.length - 1; + if (isCarrier(t)) { + while (i >= 0) { + const f = t.frames[i--]; + if (f instanceof NativeFrame && continuationEntry(t.vm, f)?.virtual) break; + } + } + for (; i >= 0 && (maxDepth < 0 || out.length < maxDepth); i--) { + const f = t.frames[i]; + if (f instanceof NativeFrame) { + if (continuationEntry(t.vm, f)) continue; + if (f.m) out.push({ m: f.m, pc: -2, at: i }); + } else out.push({ m: f.m, pc: f.pc, at: i }); + } + return out; +} + +interface Block { + start: number; + end: number; + slot: number; +} + +const blockCache = new WeakMap(); + +// The synchronized blocks of a method, as compilers emit them: a catch-any +// handler that starts with "astore; aload slot; monitorexit", where slot holds +// the lock object from the monitorenter until the monitorexit. +function syncBlocks(m: JMethod): Block[] { + let blocks = blockCache.get(m); + if (blocks) return blocks; + blocks = []; + const code = m.code!.code; + const slotAt = (pc: number, base: number, op: number): [number, number] | null => + code[pc] >= base && code[pc] <= base + 3 ? [code[pc] - base, 1] : code[pc] === op ? [code[pc + 1], 2] : null; + for (const h of m.code!.handlers) { + if (h.catchType !== 0) continue; + const store = slotAt(h.handler, 0x4b, 0x3a); + const load = store && slotAt(h.handler + store[1], 0x2a, 0x19); + if (!load || code[h.handler + store![1] + load[1]] !== 0xc3) continue; + blocks.push({ start: h.start, end: h.end, slot: load[0] }); + } + blockCache.set(m, blocks); + return blocks; +} + +// Monitors each frame locked, newest first as javaVFrame::locked_monitors +// lists them, found from the frame's pc and the locals that hold the objects. +// A monitor the thread owns that no frame accounts for is listed with depth -1. +function lockedMonitors(t: JThread, frames: Shown[]): { o: JObject; depth: number }[] { + const byFrame = new Map(); + const accounted = new Map(); + const owned = new Set(); + t.frames.forEach((f, at) => { + if (!(f instanceof Frame)) return; + const held: JObject[] = []; + const locals: any[] = f.locals ?? []; + for (const v of locals) if (v instanceof JObject && v.mon?.owner === t) owned.add(v); + if (locals.length) { + const active = syncBlocks(f.m).filter((b) => b.start <= f.pc && f.pc < b.end); + // a handler's own range is listed too, so one lock can appear twice + const slots = [...new Set(active.sort((x, y) => y.start - x.start).map((b) => b.slot))]; + for (const slot of slots) { + const v = locals[slot]; + if (v instanceof JObject && v.mon?.owner === t) held.push(v); + } + } + if (f.lock && !f.needLock && f.lock.mon?.owner === t) held.push(f.lock); + for (const o of held) accounted.set(o, (accounted.get(o) ?? 0) + 1); + byFrame.set(at, held); + }); + const out: { o: JObject; depth: number }[] = []; + frames.forEach((fr, depth) => { + for (const o of byFrame.get(fr.at) ?? []) out.push({ o, depth }); + }); + for (const o of owned) { + for (let n = o.mon!.count - (accounted.get(o) ?? 0); n > 0; n--) out.push({ o, depth: -1 }); + } + return out; +} + +interface Snapshot { + thread: JThread; + status: number; + blocker: JObject | null; + owner: JObject | null; + frames: Shown[]; +} + +// ThreadSnapshot::initialize +function snapshot(vm: JVM, t: JThread, maxDepth: number): Snapshot { + let status = threadStatus(vm, t); + let blocker: JObject | null = null; + let owner: JObject | null = null; + if (isCarrier(t)) { + status = STATUS_WAITING; + blocker = owner = t.mounted; + } else if (status === STATUS_BLOCKED || status === STATUS_WAITING || status === STATUS_TIMED_WAITING) { + const o = t.waitingOn ?? t.pendingOn; + if (o === null) status = STATUS_RUNNABLE; + else { + const holder = o.mon?.owner ?? null; + if (holder === null && status === STATUS_BLOCKED) status = STATUS_RUNNABLE; + else { + blocker = o; + owner = holder?.obj ?? null; + } + } + } else if (status === STATUS_PARKED || status === STATUS_PARKED_TIMED) { + blocker = parkBlocker(vm, t); + if (isSynchronizer(vm, blocker)) owner = synchronizerOwner(vm, blocker!); + } + return { thread: t, status, blocker, owner, frames: threadFrames(t, maxDepth) }; +} + +function stackTraceOf(t: JThread, frames: Shown[]): JObject { + const vm = t.vm; + const ste = vm.loadBoot("java/lang/StackTraceElement")!; + return vm.newObjectArray("java/lang/StackTraceElement", frames.map(({ m, pc }) => { + const e = vm.newObject(ste); + fillElement(t, e, m.cls, m.name, pc, m); + return e; + })); +} + +interface Locks { + monitors: { o: JObject; depth: number }[]; + syncs: JObject[]; +} + +function* threadInfo(t: JThread, s: Snapshot, withStack: boolean, locks: Locks | null): NativeGen { + const vm = t.vm; + const c = vm.loadBoot(THREAD_INFO)!; + if (c.state !== INITIALIZED) yield* vm.initGen(t, c); + const o = vm.newObject(c); + // contention monitoring is off, so there are counts but no times + const args = [o, s.thread.obj, s.status, s.blocker, s.owner, BigInt(s.thread.blockedCount), -1n, BigInt(s.thread.waitedCount), -1n, + withStack ? stackTraceOf(t, s.frames) : null]; + if (locks === null) yield t.call(c.methodMap.get(THREAD_INFO_INIT)!, ...args); + else { + const ints = vm.newArray(vm.primClass("I"), locks.monitors.length); + locks.monitors.forEach((x, i) => ((ints.a as Int32Array)[i] = x.depth)); + args.push( + vm.newObjectArray("java/lang/Object", locks.monitors.map((x) => x.o)), + ints, + vm.newObjectArray("java/lang/Object", locks.syncs), + ); + yield t.call(c.methodMap.get(THREAD_INFO_LOCKS_INIT)!, ...args); + } + return o; +} + +// The ownable synchronizers each thread owns, by owner. HotSpot finds them by +// inspecting the heap; so does this, from the class statics and thread stacks, +// which keeps the lock and unlock paths as they are. +function ownedSynchronizers(vm: JVM): Map { + const aos = vm.loadBoot(AOS)!; + const slot = aos.findField("exclusiveOwnerThread")!.slot; + const isSync = new Map(); + const seen = new Set(); + const classes = new Set(); + const stack: JObject[] = []; + const out = new Map(); + const visit = (v: unknown) => { + if (v instanceof JObject && !seen.has(v)) { + seen.add(v); + stack.push(v); + } + }; + const visitClass = (c: JClass) => { + if (classes.has(c)) return; + classes.add(c); + if (c.mirror) visit(c.mirror); + for (const v of c.statics) visit(v); + }; + for (const c of vm.boot.values()) visitClass(c); + for (const t of vm.threads) { + visit(t.obj); + visit(t.mounted); + for (const f of t.frames) { + if (!(f instanceof Frame)) continue; + visitClass(f.m.cls); + for (const v of f.locals ?? []) visit(v); + for (let i = 0; i < f.sp; i++) visit(f.stack?.[i]); + visit(f.lock); + } + } + while (stack.length > 0) { + const o = stack.pop()!; + let sync = isSync.get(o.cls); + if (sync === undefined) isSync.set(o.cls, (sync = o.cls.isAssignableTo(aos))); + if (sync) { + const owner = o.f[slot]; + if (owner) { + const list = out.get(owner); + if (list) list.push(o); + else out.set(owner, [o]); + } + } + visitClass(o.cls); + if (o instanceof JArray) { + if (Array.isArray(o.a)) for (const v of o.a) visit(v); + } else for (const v of o.f) visit(v); + // a class loader keeps the classes it defined + const x = o.x; + if (x instanceof JClass) visitClass(x); + else if (x?.classes instanceof Map) for (const c of x.classes.values()) visitClass(c); + } + return out; +} + +function checkIds(ids: JArray) { + for (const id of ids.a as BigInt64Array) { + if (id <= 0n) throw new PendingThrow("java/lang/IllegalArgumentException", "Invalid thread ID entry"); + } +} + +function* dumpThreads(t: JThread, ids: JArray | null, monitors: boolean, syncs: boolean, maxDepth: number): NativeGen { + const vm = t.vm; + if (ids) checkIds(ids); + const threads = ids + ? [...(ids.a as BigInt64Array)].map((id) => threadById(vm, id)) + : liveThreads(vm).filter((x) => threadStatus(vm, x) !== STATUS_TERMINATED); + const owned = syncs ? ownedSynchronizers(vm) : null; + // all snapshots are taken before any Java code runs + const dumps = threads.map((x) => { + if (!x) return null; + const s = snapshot(vm, x, maxDepth); + const locks: Locks = { + monitors: monitors ? lockedMonitors(x, s.frames) : [], + syncs: syncs ? owned!.get(x.obj!) ?? [] : [], + }; + return { s, locks }; + }); + const out: (JObject | null)[] = []; + for (const d of dumps) { + out.push(d ? yield* threadInfo(t, d.s, true, d.locks) : null); + } + return vm.newObjectArray(THREAD_INFO, out); +} + +// Cycles of threads waiting on each other, as ThreadService::find_deadlocks_at_safepoint +// finds them. Only monitors, or ownable synchronizers too when asked. +function findDeadlocks(vm: JVM, synchronizers: boolean): JObject | null { + const pending = (x: JThread) => x.pendingOn; + const blocker = (x: JThread) => (synchronizers ? parkBlocker(vm, x) : null); + const dfn = new Map(); + const cycles: JThread[] = []; + let next = 0; + for (const start of liveThreads(vm)) { + if (isCarrier(start) || dfn.has(start)) continue; + const first = next; + dfn.set(start, next++); + let prev = start; + let cur: JThread | null = start; + const path: JThread[] = []; + let mon = pending(start); + let park = blocker(start); + while (cur && (mon || park)) { + path.push(cur); + if (mon) { + const owner = mon.mon?.owner ?? null; + if (owner !== null) cur = owner.state === TERMINATED ? null : owner; + } else if (isSynchronizer(vm, park)) { + cur = platformThread(synchronizerOwner(vm, park!)); + } else cur = null; + if (cur === null || isCarrier(cur)) break; + const d = dfn.get(cur); + if (d === undefined) dfn.set(cur, next++); + else if (d < first || cur === prev) break; + else { + cycles.push(...path); + break; + } + prev = cur; + mon = pending(cur); + park = blocker(cur); + } + } + return cycles.length === 0 ? null : vm.newObjectArray(TH, cycles.map((x) => x.obj!)); +} + const V = "sun/management/VMManagementImpl."; const OS = "com/sun/management/internal/OperatingSystemImpl."; const P = "sun/management/MemoryPoolImpl."; @@ -150,7 +490,11 @@ const T = "sun/management/ThreadImpl."; export const natives: Natives = { [`${V}getVersion0()Ljava/lang/String;`]: (t) => t.vm.str("4.0"), - [`${V}initOptionalSupportFields()V`]: () => undefined, + [`${V}initOptionalSupportFields()V`]: (t) => { + const c = t.vm.loadBoot("sun/management/VMManagementImpl")!; + t.vm.setStatic(c, "objectMonitorUsageSupport", 1); + t.vm.setStatic(c, "synchronizerUsageSupport", 1); + }, [`${V}isThreadContentionMonitoringEnabled()Z`]: () => 0, [`${V}isThreadCpuTimeEnabled()Z`]: () => 0, [`${V}isThreadAllocatedMemoryEnabled()Z`]: () => 0, @@ -234,8 +578,25 @@ export const natives: Natives = { [`${T}setThreadCpuTimeEnabled0(Z)V`]: () => undefined, [`${T}setThreadAllocatedMemoryEnabled0(Z)V`]: () => undefined, [`${T}setThreadContentionMonitoringEnabled0(Z)V`]: () => undefined, - [`${T}findMonitorDeadlockedThreads0()[Ljava/lang/Thread;`]: () => null, - [`${T}findDeadlockedThreads0()[Ljava/lang/Thread;`]: () => null, + [`${T}findMonitorDeadlockedThreads0()[Ljava/lang/Thread;`]: (t) => findDeadlocks(t.vm, false), + [`${T}findDeadlockedThreads0()[Ljava/lang/Thread;`]: (t) => findDeadlocks(t.vm, true), + [`${T}getThreadInfo1([JI[Ljava/lang/management/ThreadInfo;)V`]: function* (t, [ids, maxDepth, result]): NativeGen { + if (!ids || !result) throw new PendingThrow("java/lang/NullPointerException"); + if (maxDepth < -1) throw new PendingThrow("java/lang/IllegalArgumentException", "Invalid maxDepth"); + checkIds(ids); + if (ids.a.length !== result.a.length) { + throw new PendingThrow("java/lang/IllegalArgumentException", + "The length of the given ThreadInfo array does not match the length of the given array of thread IDs"); + } + const vm = t.vm; + const snaps = [...(ids.a as BigInt64Array)].map((id) => { + const x = threadById(vm, id); + return x && snapshot(vm, x, maxDepth); + }); + for (const [i, s] of snaps.entries()) if (s) result.a[i] = yield* threadInfo(t, s, maxDepth !== 0, null); + }, + [`${T}dumpThreads0([JZZI)[Ljava/lang/management/ThreadInfo;`]: (t, [ids, monitors, syncs, maxDepth]) => + dumpThreads(t, ids, monitors === 1, syncs === 1, maxDepth), [`${T}resetPeakThreadCount0()V`]: (t) => void (t.vm.peakThreads = liveThreads(t.vm).length), [`${T}resetContentionTimes0(J)V`]: () => undefined, "sun/management/HotspotThread.getInternalThreadCount()I": () => 0, diff --git a/src/snapshot.ts b/src/snapshot.ts index f5257d6..a0b0ebd 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -498,9 +498,11 @@ function snapshotFds(value: unknown): unknown { // A waiter woken as if spuriously: takes its monitor back with the count it gave up. function* rewait(t: JThread, o: JObject, count: number): NativeGen { const mon = o.mon!; + t.waitingOn = o; while (mon.owner !== null) yield t.block(new Promise((r) => mon.entry.push(r))); mon.owner = t; mon.count = count; + t.waitingOn = null; t.vm.setThreadStatus(t, STATUS_RUNNABLE); } diff --git a/src/thread.ts b/src/thread.ts index 59e1cee..472df5c 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -113,6 +113,12 @@ export class JThread { frames: AnyFrame[] = []; // waiting to enter a monitor blockedOnMonitor = false; + // the monitor it is blocked entering, and the one it is in wait() on + pendingOn: JObject | null = null; + waitingOn: JObject | null = null; + // contended monitor entries, and waits and sleeps, for ThreadInfo + blockedCount = 0; + waitedCount = 0; // monitor entries not yet exited, and Continuation.pin() depth held = 0; pins = 0; diff --git a/test/java/ThreadInfos.java b/test/java/ThreadInfos.java new file mode 100644 index 0000000..8acc6e5 --- /dev/null +++ b/test/java/ThreadInfos.java @@ -0,0 +1,226 @@ +import java.lang.management.ManagementFactory; +import java.lang.management.MonitorInfo; +import java.lang.management.LockInfo; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +// ThreadMXBean thread descriptions: deadlock detection over monitors and +// ownable synchronizers, and getThreadInfo and dumpAllThreads with locks. +// Hash codes and line numbers differ between runs, so they are not printed. +public class ThreadInfos { + static final ThreadMXBean MX = ManagementFactory.getThreadMXBean(); + + static class Res {} + + static class Holder extends Thread { + Holder(String name) { super(name); setDaemon(true); } + } + + static class MonitorLocker extends Holder { + final Object first, second; + final CyclicBarrier barrier; + MonitorLocker(String name, Object first, Object second, CyclicBarrier barrier) { + super(name); + this.first = first; + this.second = second; + this.barrier = barrier; + } + @Override public void run() { + synchronized (first) { + await(barrier); + synchronized (second) { } + } + } + } + + static class LockLocker extends Holder { + final ReentrantLock first, second; + final CyclicBarrier barrier; + LockLocker(String name, ReentrantLock first, ReentrantLock second, CyclicBarrier barrier) { + super(name); + this.first = first; + this.second = second; + this.barrier = barrier; + } + @Override public void run() { + first.lock(); + await(barrier); + second.lock(); + } + } + + static class Waiter extends Holder { + final Object lock; + final long millis; + Waiter(String name, Object lock, long millis) { super(name); this.lock = lock; this.millis = millis; } + @Override public void run() { + synchronized (lock) { + try { lock.wait(millis); } catch (InterruptedException e) { } + } + } + } + + static class Blocked extends Holder { + final Object lock; + Blocked(String name, Object lock) { super(name); this.lock = lock; } + @Override public void run() { synchronized (lock) { } } + } + + static class Sleeper extends Holder { + Sleeper(String name) { super(name); } + @Override public void run() { try { Thread.sleep(1_000_000); } catch (InterruptedException e) { } } + } + + static class Parker extends Holder { + final Object blocker; + Parker(String name, Object blocker) { super(name); this.blocker = blocker; } + @Override public void run() { + if (blocker == null) LockSupport.park(); else LockSupport.park(blocker); + } + } + + static void await(CyclicBarrier b) { + try { b.await(); } catch (Exception e) { throw new RuntimeException(e); } + } + + static void until(Thread t, Thread.State state) throws Exception { + while (t.getState() != state) Thread.sleep(5); + } + + static List names(long[] ids) { + List out = new ArrayList<>(); + if (ids != null) for (ThreadInfo i : MX.getThreadInfo(ids)) out.add(i.getThreadName()); + out.sort(null); + return out; + } + + static String lock(LockInfo l) { return l == null ? "-" : l.getClassName(); } + + static String frames(ThreadInfo i, int n) { + StackTraceElement[] st = i.getStackTrace(); + StringBuilder sb = new StringBuilder(); + for (int k = 0; k < Math.min(n, st.length); k++) { + if (k > 0) sb.append(" < "); + sb.append(st[k].getClassName()).append('.').append(st[k].getMethodName()); + } + return sb.toString(); + } + + static void describe(Thread t, boolean locks) { + ThreadInfo i = locks + ? MX.getThreadInfo(new long[] { t.threadId() }, true, true)[0] + : MX.getThreadInfo(t.threadId(), 3); + System.out.println(i.getThreadName() + ": " + i.getThreadState() + " lock=" + lock(i.getLockInfo()) + + " owner=" + i.getLockOwnerName() + " top=" + frames(i, 3)); + if (locks) { + for (MonitorInfo m : i.getLockedMonitors()) { + System.out.println(" monitor " + m.getClassName() + " depth=" + m.getLockedStackDepth() + + " frame=" + m.getLockedStackFrame().getMethodName()); + } + List syncs = new ArrayList<>(); + for (LockInfo s : i.getLockedSynchronizers()) syncs.add(s.getClassName()); + syncs.sort(null); + System.out.println(" synchronizers " + syncs); + } + } + + static synchronized void dumpInMethod() { + ThreadInfo i = MX.getThreadInfo(new long[] { Thread.currentThread().threadId() }, true, false)[0]; + for (MonitorInfo m : i.getLockedMonitors()) { + System.out.println(" method monitor " + m.getClassName() + " depth=" + m.getLockedStackDepth() + + " frame=" + m.getLockedStackFrame().getMethodName()); + } + } + + public static void main(String[] args) throws Exception { + // monitor deadlock + Object x = new Res(), y = new Res(); + CyclicBarrier b1 = new CyclicBarrier(2); + Thread ma = new MonitorLocker("m-a", x, y, b1), mb = new MonitorLocker("m-b", y, x, b1); + ma.start(); + mb.start(); + until(ma, Thread.State.BLOCKED); + until(mb, Thread.State.BLOCKED); + System.out.println("monitor deadlock: " + names(MX.findMonitorDeadlockedThreads())); + System.out.println("deadlock: " + names(MX.findDeadlockedThreads())); + describe(ma, true); + describe(mb, true); + + // ReentrantLock deadlock: monitors alone do not see it + ReentrantLock l1 = new ReentrantLock(), l2 = new ReentrantLock(); + CyclicBarrier b2 = new CyclicBarrier(2); + Thread ra = new LockLocker("r-a", l1, l2, b2), rb = new LockLocker("r-b", l2, l1, b2); + ra.start(); + rb.start(); + until(ra, Thread.State.WAITING); + until(rb, Thread.State.WAITING); + System.out.println("monitor deadlock: " + names(MX.findMonitorDeadlockedThreads())); + System.out.println("deadlock: " + names(MX.findDeadlockedThreads())); + describe(ra, true); + describe(rb, true); + + // waiting, blocked, sleeping and parked threads + Object cond = new Res(); + Thread w = new Waiter("waiter", cond, 0), tw = new Waiter("timed-waiter", new Res(), 1_000_000); + w.start(); + tw.start(); + until(w, Thread.State.WAITING); + until(tw, Thread.State.TIMED_WAITING); + describe(w, true); + describe(tw, false); + Thread s = new Sleeper("sleeper"), p = new Parker("parker", null), pb = new Parker("parker-blocker", new Res()); + s.start(); + p.start(); + pb.start(); + until(s, Thread.State.TIMED_WAITING); + until(p, Thread.State.WAITING); + until(pb, Thread.State.WAITING); + describe(s, false); + describe(p, true); + describe(pb, true); + + // the calling thread's own locks, in one frame and in a synchronized method + Object mon = new Res(); + ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); + Thread blocked; + synchronized (mon) { + synchronized (cond) { + rw.writeLock().lock(); + blocked = new Blocked("blocked", mon); + blocked.start(); + until(blocked, Thread.State.BLOCKED); + describe(blocked, false); + describe(Thread.currentThread(), true); + dumpInMethod(); + rw.writeLock().unlock(); + } + } + until(blocked, Thread.State.TERMINATED); + describe(Thread.currentThread(), true); + + // depth limits, missing threads, and dumpAllThreads + long id = ma.threadId(); + System.out.println("depth 0: " + MX.getThreadInfo(id, 0).getStackTrace().length); + System.out.println("depth 1: " + MX.getThreadInfo(id, 1).getStackTrace().length); + System.out.println("blocked, depth 0: " + MX.getThreadInfo(id, 0).getThreadState()); + System.out.println("terminated: " + MX.getThreadInfo(blocked.threadId())); + System.out.println("never started: " + MX.getThreadInfo(Long.MAX_VALUE)); + ThreadInfo[] all = MX.dumpAllThreads(true, true); + List found = new ArrayList<>(); + for (ThreadInfo i : all) { + if (Arrays.asList("main", "m-a", "m-b", "r-a", "r-b", "waiter", "sleeper", "blocked").contains(i.getThreadName())) { + found.add(i.getThreadName()); + } + } + found.sort(null); + System.out.println("dumpAllThreads: " + found); + System.out.println("no locks: " + (MX.dumpAllThreads(false, false).length >= found.size())); + } +} diff --git a/test/jtreg/passing.txt b/test/jtreg/passing.txt index 512c39a..2ca628c 100644 --- a/test/jtreg/passing.txt +++ b/test/jtreg/passing.txt @@ -1083,6 +1083,10 @@ java/lang/Thread/ThreadStateTest.java java/lang/Thread/ThreadStopTest.java java/lang/Thread/UncaughtExceptionsTest.java java/lang/Thread/virtual/BigStackChunk.java +java/lang/Thread/virtual/CarrierThreadInfo.java +java/lang/Thread/virtual/CarrierThreadInfo.java#2 +java/lang/Thread/virtual/CarrierThreadInfo.java#3 +java/lang/Thread/virtual/CarrierThreadInfo.java#4 java/lang/Thread/virtual/Collectable.java java/lang/Thread/virtual/CustomScheduler.java java/lang/Thread/virtual/Locking.java @@ -1331,14 +1335,21 @@ java/lang/management/RuntimeMXBean/UpTime.java java/lang/management/ThreadMXBean/AllThreadIds.java java/lang/management/ThreadMXBean/DisableTest.java java/lang/management/ThreadMXBean/EnableTest.java +java/lang/management/ThreadMXBean/FindDeadlocks.java +java/lang/management/ThreadMXBean/FindMonitorDeadlock.java java/lang/management/ThreadMXBean/InvalidThreadID.java java/lang/management/ThreadMXBean/LockedMonitors.java java/lang/management/ThreadMXBean/LockedSynchronizers.java +java/lang/management/ThreadMXBean/Locks.java +java/lang/management/ThreadMXBean/MaxDepthForThreadInfoTest.java java/lang/management/ThreadMXBean/MyOwnSynchronizer.java java/lang/management/ThreadMXBean/ResetPeakThreadCount.java java/lang/management/ThreadMXBean/SharedSynchronizer.java +java/lang/management/ThreadMXBean/ThreadBlockedCount.java java/lang/management/ThreadMXBean/ThreadCounts.java java/lang/management/ThreadMXBean/ThreadCpuTime.java +java/lang/management/ThreadMXBean/ThreadDaemonTest.java +java/lang/management/ThreadMXBean/ThreadInfoArray.java java/lang/management/ThreadMXBean/ThreadLists.java java/lang/management/ThreadMXBean/ThreadUserTime.java java/lang/module/AutomaticModulesTest.java diff --git a/test/jtreg/times.tsv b/test/jtreg/times.tsv index 0cc43e7..3add4d1 100644 --- a/test/jtreg/times.tsv +++ b/test/jtreg/times.tsv @@ -520,6 +520,7 @@ 9610 java/lang/IntegralPrimitiveToString.java 9603 java/text/Collator/RuleBasedCollatorTest.java 9602 java/util/jar/JarFile/VerifySignedJar.java +9600 java/lang/Thread/virtual/CarrierThreadInfo.java#4 9599 java/util/Locale/PreserveTagCase.java 9592 java/nio/channels/Selector/SelectorLimit.java 9578 java/util/ArrayList/AddAll.java @@ -792,6 +793,7 @@ 7295 java/beans/XMLEncoder/javax_swing_plaf_BorderUIResource_TitledBorderUIResource.java 7293 java/util/Calendar/bug4514831.java 7292 java/util/Properties/InitialCapacity.java +7284 java/lang/management/ThreadMXBean/ThreadInfoArray.java 7278 java/beans/PropertyEditor/TestEnumClassJava.java 7277 javax/crypto/NullCipher/TestNPE.java 7276 java/beans/PropertyEditor/TestShortTypeJava.java @@ -910,6 +912,7 @@ 6693 java/lang/management/CompositeData/ThreadInfoCompositeData.java 6688 java/beans/XMLEncoder/java_util_Collections_UnmodifiableSet.java 6685 java/io/PrintStream/InheritEncodingTest.java +6667 java/lang/Thread/virtual/CarrierThreadInfo.java#3 6663 java/util/Hashtable/DeserializedLength.java 6656 java/lang/String/concat/ImplicitStringConcatArgCount.java 6649 java/time/tck/java/time/chrono/TCKHijrahEra.java @@ -1035,6 +1038,7 @@ 6114 java/util/logging/Logger/getLogger/TestInferCaller.java 6108 java/text/Format/NumberFormat/Bug4990596.java 6100 java/lang/module/ModuleReferenceTest.java +6092 java/lang/Thread/virtual/CarrierThreadInfo.java 6080 java/net/httpclient/HeadersTest.java 6080 java/io/Serializable/records/StreamRefTest.java 6076 java/lang/module/ClassFileVersionsTest.java @@ -1064,6 +1068,7 @@ 5981 java/text/Format/DecimalFormat/ToLocalizedPatternTest.java 5979 java/time/tck/java/time/format/TCKInstantPrinterParser.java 5972 java/util/concurrent/LinkedTransferQueue/SpliteratorTraverseAddRemoveTest.java +5969 java/lang/Thread/virtual/CarrierThreadInfo.java#2 5958 java/io/Serializable/sanityCheck/SanityCheck.java 5953 java/io/OutputStream/NullOutputStream.java 5935 java/nio/Buffer/CopyDirectMemory.java @@ -1971,6 +1976,7 @@ 3454 java/math/BigDecimal/EqualsTests.java 3453 java/security/cert/PKIXRevocationChecker/OcspUnauthorized.java 3451 java/lang/ref/ReferenceEnqueue.java +3451 java/lang/management/ThreadMXBean/FindDeadlocks.java 3449 java/io/InputStreamReader/One.java 3447 java/util/TimeZone/IDTest.java 3446 java/io/FileWriter/ConstructorTest.java @@ -2669,6 +2675,7 @@ 2587 java/io/RandomAccessFile/Close.java 2586 java/util/Deque/ChorusLine.java 2585 java/net/CookieHandler/TestHttpCookie.java +2584 java/lang/management/ThreadMXBean/Locks.java 2580 java/util/concurrent/BlockingQueue/DrainToFails.java 2580 java/lang/StackTraceElement/PublicConstructor.java 2580 java/beans/PropertyEditor/Test7087876.java @@ -2708,6 +2715,7 @@ 2548 java/beans/XMLDecoder/spec/TestJava.java 2546 java/util/concurrent/ConcurrentQueues/GCRetention.java 2545 java/util/zip/ZipEntry/Constructor.java +2544 java/lang/management/ThreadMXBean/ThreadDaemonTest.java 2543 java/security/cert/pkix/nameConstraintsMinMax/VerifyNameConstraints.java 2543 java/beans/PropertyEditor/TestFloatClassValue.java 2541 javax/xml/jaxp/transform/8004476/XSLTExFuncTest.java @@ -3130,6 +3138,7 @@ 2050 java/lang/invoke/lambda/LambdaClassSynthetic.java 2049 java/nio/channels/AsynchronousChannelGroup/Identity.java 2048 java/lang/ClassLoader/LibraryPathProperty.java +2048 java/lang/management/ThreadMXBean/ThreadBlockedCount.java 2047 java/lang/RuntimeTests/Version/Basic.java 2047 java/lang/reflect/Parameter/NoName.java 2046 javax/crypto/spec/IvParameterSpec/NullIV.java @@ -3162,6 +3171,7 @@ 2020 java/lang/StrictMath/Expm1Tests.java 2018 java/lang/invoke/accessProtectedSuper/Test.java 2016 java/lang/Long/GetLong.java +2015 java/lang/management/ThreadMXBean/FindMonitorDeadlock.java 2014 java/util/Properties/LoadSeparators.java 2014 java/io/File/DeleteOnExitLong.java 2013 java/io/Serializable/readPastObject/ReadPastObject.java @@ -3283,6 +3293,7 @@ 1873 java/lang/StrictMath/Log10Tests.java 1869 java/net/URL/Protocol.java 1867 java/lang/Math/InverseTrigTests.java +1867 java/lang/management/ThreadMXBean/MaxDepthForThreadInfoTest.java 1866 java/util/concurrent/ThreadPoolExecutor/ThrowingTasks.java 1866 java/util/concurrent/Phaser/PhaseOverflow.java 1866 java/lang/reflect/Field/Set.java