c58fbef05e
Reviewed-by: cjplummer, lmesnik
518 lines
19 KiB
Java
518 lines
19 KiB
Java
/*
|
|
* Copyright (c) 2003, 2024, 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.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
/*
|
|
* @test
|
|
* @bug 4530538
|
|
* @summary Basic unit test of ThreadInfo.getLockName()
|
|
* and ThreadInfo.getLockOwnerName()
|
|
* @author Mandy Chung
|
|
* @author Jaroslav Bachorik
|
|
*
|
|
* @library /test/lib
|
|
*
|
|
* @run main/othervm Locks
|
|
*/
|
|
import java.lang.management.*;
|
|
import java.util.Arrays;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.concurrent.Phaser;
|
|
import java.util.function.Predicate;
|
|
import jdk.test.lib.LockFreeLogger;
|
|
|
|
public class Locks {
|
|
|
|
private static class ObjectA { }
|
|
private static class ObjectB { }
|
|
|
|
private static final Object OBJA = new ObjectA();
|
|
private static final Object OBJB = new ObjectB();
|
|
private static final EnhancedWaiter OBJC = new EnhancedWaiter();
|
|
private static final ThreadMXBean TM = ManagementFactory.getThreadMXBean();
|
|
private static final LockFreeLogger LOGGER = new LockFreeLogger();
|
|
|
|
private static String getLockName(Object lock) {
|
|
if (lock == null) return null;
|
|
|
|
return lock.getClass().getName() + '@' +
|
|
Integer.toHexString(System.identityHashCode(lock));
|
|
}
|
|
|
|
private static void assertNoLock(Thread t) {
|
|
if (t == null) {
|
|
return;
|
|
}
|
|
String name = t.getName();
|
|
Optional<ThreadInfo> result = Arrays.stream(
|
|
TM.getThreadInfo(TM.getAllThreadIds(), true, true))
|
|
.filter(Objects::nonNull)
|
|
.filter(i -> name.equals(i.getLockOwnerName()))
|
|
/* Carrier Thread can hold a lock on a VirtualThread, which we ignore: */
|
|
.filter(i -> !i.getLockName().contains("java.lang.VirtualThread"))
|
|
.findAny();
|
|
if (result.isPresent()) {
|
|
throw new RuntimeException("Thread " + t.getName() + " is not "
|
|
+ "supposed to be hold any lock. Currently owning lock : "
|
|
+ result.get().getLockName());
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Handy debug function to check if error condition is because of test code or not.
|
|
*/
|
|
private static void printStackTrace(Thread thread) {
|
|
if (thread == null) {
|
|
return;
|
|
}
|
|
StackTraceElement[] stackTrace = thread.getStackTrace();
|
|
log("Stack dump : Thread -> " + thread.getName());
|
|
for (StackTraceElement stackTraceEl : stackTrace) {
|
|
log("\t" + stackTraceEl.toString());
|
|
}
|
|
}
|
|
|
|
private static void assertThreadState(Thread t, Thread.State expectedState) {
|
|
long tid = t.getId();
|
|
Thread.State actualState = TM.getThreadInfo(tid).getThreadState();
|
|
if (!actualState.equals(expectedState)) {
|
|
if (expectedState.equals(Thread.State.BLOCKED) ||
|
|
expectedState.equals(Thread.State.WAITING))
|
|
{
|
|
int retryCount = 0;
|
|
printStackTrace(t);
|
|
do {
|
|
goSleep(100);
|
|
actualState = TM.getThreadInfo(tid).getThreadState();
|
|
} while (!actualState.equals(expectedState) && retryCount++ <= 500);
|
|
}
|
|
if (!actualState.equals(expectedState)) {
|
|
printStackTrace(t);
|
|
throw new RuntimeException("Thread " + t.getName() + " is at "
|
|
+ actualState + " state but is expected to "
|
|
+ "be in Thread.State = " + expectedState);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Do slow check if thread is blocked on a lock. It is possible that last thread
|
|
* to come out of Phaser might still be in Phaser call stack (Unsafe.park) and
|
|
* hence might eventually acquire expected lock.
|
|
*/
|
|
private static void checkBlockedObject(Thread t, Object lock, Thread owner) {
|
|
long tid = t.getId();
|
|
String lockName = TM.getThreadInfo(tid).getLockName();
|
|
final String expectedLock = (lock != null ? getLockName(lock) : null);
|
|
Predicate<String> p = (res) -> ((res != null && !res.equals(expectedLock))
|
|
|| (res == null && expectedLock != null));
|
|
|
|
if (p.test(lockName)) {
|
|
printStackTrace(t);
|
|
int retryCount = 0;
|
|
while (p.test(lockName)) {
|
|
if (retryCount++ > 500) {
|
|
printStackTrace(t);
|
|
throw new RuntimeException("Thread " + t.getName() + " is blocked on "
|
|
+ expectedLock + " but got " + lockName);
|
|
}
|
|
goSleep(100);
|
|
lockName = TM.getThreadInfo(tid).getLockName();
|
|
}
|
|
}
|
|
|
|
String lockOwnerName = TM.getThreadInfo(tid).getLockOwnerName();
|
|
final String expectedOwnerName = (owner != null ? owner.getName() : null);
|
|
|
|
p = (res) -> ((res != null && !res.equals(expectedOwnerName))
|
|
|| (res == null && expectedOwnerName != null));
|
|
if (p.test(lockOwnerName)) {
|
|
printStackTrace(t);
|
|
throw new RuntimeException("Owner of " + lock + " should be "
|
|
+ expectedOwnerName + " but got " + lockOwnerName);
|
|
}
|
|
}
|
|
|
|
private static void goSleep(long ms){
|
|
try {
|
|
Thread.sleep(ms);
|
|
} catch (InterruptedException ex) {
|
|
throw new RuntimeException(ex);
|
|
}
|
|
}
|
|
|
|
private static volatile int dummyCounter = 0;
|
|
|
|
static class LockAThread extends Thread {
|
|
private final Phaser p;
|
|
public LockAThread(Phaser p) {
|
|
super("LockAThread");
|
|
this.p = p;
|
|
setDaemon(true);
|
|
}
|
|
@Override
|
|
public void run() {
|
|
synchronized(OBJA) {
|
|
// block here while LockBThread holds OBJB
|
|
log("LockAThread about to block on OBJB");
|
|
p.arriveAndAwaitAdvance(); // Phase 1 (blocking)
|
|
synchronized(OBJB) {
|
|
dummyCounter++;
|
|
}
|
|
}
|
|
p.arriveAndAwaitAdvance(); // Phase 2 (blocking)
|
|
log("LockAThread about to exit");
|
|
// Make sure the current thread is not holding any lock
|
|
assertNoLock(this);
|
|
}
|
|
}
|
|
|
|
static class LockBThread extends Thread {
|
|
private final Phaser p;
|
|
public LockBThread(Phaser p) {
|
|
super("LockBThread");
|
|
this.p = p;
|
|
setDaemon(true);
|
|
}
|
|
@Override
|
|
public void run() {
|
|
synchronized(OBJB) {
|
|
log("LockBThread about to block on OBJC");
|
|
p.arriveAndAwaitAdvance(); // Phase 1 (blocking)
|
|
// Signal main thread about to block on OBJC
|
|
synchronized(OBJC) {
|
|
dummyCounter++;
|
|
}
|
|
}
|
|
p.arriveAndAwaitAdvance(); // Phase 2 (blocking)
|
|
log("LockBThread about to exit");
|
|
// Make sure the current thread is not holding any lock
|
|
assertNoLock(this);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Must be invoked from within a synchronized context
|
|
*/
|
|
private static class EnhancedWaiter {
|
|
|
|
boolean isNotified = false;
|
|
|
|
public void doWait() throws InterruptedException {
|
|
while (!isNotified) {
|
|
wait();
|
|
}
|
|
isNotified = false;
|
|
}
|
|
|
|
public void doNotify() {
|
|
isNotified = true;
|
|
notify();
|
|
}
|
|
}
|
|
|
|
private static WaitingThread waiter;
|
|
private static final Object ready = new Object();
|
|
private static CheckerThread checker;
|
|
|
|
static class WaitingThread extends Thread {
|
|
private final Phaser p;
|
|
|
|
volatile boolean waiting = false;
|
|
|
|
public WaitingThread(Phaser p) {
|
|
super("WaitingThread");
|
|
this.p = p;
|
|
setDaemon(true);
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
synchronized (OBJC) {
|
|
log("WaitingThread about to wait on OBJC");
|
|
// Signal checker thread, about to wait on OBJC.
|
|
waiting = false;
|
|
p.arriveAndAwaitAdvance(); // Phase 1 (waiting)
|
|
waiting = true;
|
|
OBJC.doWait();
|
|
|
|
// block until CheckerThread finishes checking
|
|
log("WaitingThread about to block on ready");
|
|
// signal checker thread that it is about acquire
|
|
// object ready.
|
|
p.arriveAndAwaitAdvance(); // Phase 2 (waiting)
|
|
synchronized (ready) {
|
|
dummyCounter++;
|
|
}
|
|
}
|
|
synchronized (OBJC) {
|
|
// signal checker thread, about to wait on OBJC
|
|
waiting = false;
|
|
p.arriveAndAwaitAdvance(); // Phase 3 (waiting)
|
|
waiting = true;
|
|
OBJC.doWait();
|
|
}
|
|
log("WaitingThread about to exit waiting on OBJC 2");
|
|
} catch (InterruptedException e) {
|
|
// test failed and this thread was interrupted
|
|
}
|
|
}
|
|
|
|
public void waitForWaiting() {
|
|
p.arriveAndAwaitAdvance();
|
|
while (!waiting) {
|
|
goSleep(10);
|
|
}
|
|
waitForState(State.WAITING);
|
|
}
|
|
|
|
public void waitForBlocked() {
|
|
p.arriveAndAwaitAdvance();
|
|
waitForState(State.BLOCKED);
|
|
}
|
|
|
|
private void waitForState(Thread.State state) {
|
|
while (!waiter.isInterrupted() && waiter.getState() != state) {
|
|
Thread.yield();
|
|
}
|
|
}
|
|
}
|
|
|
|
static class CheckerThread extends Thread {
|
|
private Exception result = null;
|
|
|
|
public CheckerThread() {
|
|
super("CheckerThread");
|
|
setDaemon(true);
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
synchronized (ready) {
|
|
// wait until WaitingThread about to wait for OBJC
|
|
waiter.waitForWaiting(); // Phase 1 (waiting)
|
|
assertThreadState(waiter, Thread.State.WAITING);
|
|
checkBlockedObject(waiter, OBJC, null);
|
|
synchronized (OBJC) {
|
|
OBJC.doNotify();
|
|
}
|
|
// wait for waiter thread to about to enter
|
|
// synchronized object ready.
|
|
waiter.waitForBlocked(); // Phase 2 (waiting)
|
|
assertThreadState(waiter, Thread.State.BLOCKED);
|
|
checkBlockedObject(waiter, ready, this);
|
|
}
|
|
|
|
// wait for signal from waiting thread that it is about
|
|
// wait for OBJC.
|
|
waiter.waitForWaiting(); // Phase 3 (waiting)
|
|
synchronized (OBJC) {
|
|
assertThreadState(waiter, Thread.State.WAITING);
|
|
checkBlockedObject(waiter, OBJC, Thread.currentThread());
|
|
OBJC.doNotify();
|
|
}
|
|
} catch (Exception e) {
|
|
waiter.interrupt();
|
|
result = e;
|
|
}
|
|
}
|
|
|
|
Exception result() {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public static void main(String args[]) throws Exception {
|
|
try {
|
|
Thread mainThread = Thread.currentThread();
|
|
// Test uncontested case
|
|
LockAThread t1;
|
|
LockBThread t2;
|
|
|
|
Phaser p = new Phaser(3);
|
|
synchronized(OBJC) {
|
|
// Make sure the main thread is not holding any lock (except possibly a VirtualThread)
|
|
assertNoLock(mainThread);
|
|
|
|
// Test deadlock case
|
|
// t1 holds lockA and attempts to lock B
|
|
// t2 holds lockB and attempts to lock C
|
|
t1 = new LockAThread(p);
|
|
t1.start();
|
|
|
|
t2 = new LockBThread(p);
|
|
t2.start();
|
|
|
|
p.arriveAndAwaitAdvance(); // Phase 1 (blocking)
|
|
|
|
assertThreadState(t2, Thread.State.BLOCKED);
|
|
if (!mainThread.isVirtual()) {
|
|
// ThreadInfo not available for Virtual Threads.
|
|
checkBlockedObject(t2, OBJC, mainThread);
|
|
}
|
|
assertThreadState(t1, Thread.State.BLOCKED);
|
|
checkBlockedObject(t1, OBJB, t2);
|
|
|
|
long[] expectedThreads = new long[mainThread.isVirtual() ? 2: 3];
|
|
expectedThreads[0] = t1.getId(); // blocked on lockB
|
|
expectedThreads[1] = t2.getId(); // owner of lockB blocking on lockC
|
|
if (!mainThread.isVirtual()) {
|
|
// ThreadInfo not available for Virtual Threads.
|
|
expectedThreads[2] = mainThread.getId(); // owner of lockC
|
|
}
|
|
findThreadsBlockedOn(OBJB, expectedThreads);
|
|
}
|
|
p.arriveAndAwaitAdvance(); // Phase 2 (blocking)
|
|
|
|
p = new Phaser(2);
|
|
// Test Object.wait() case
|
|
waiter = new WaitingThread(p);
|
|
waiter.start();
|
|
checker = new CheckerThread();
|
|
checker.start();
|
|
try {
|
|
waiter.join();
|
|
checker.join();
|
|
} catch (InterruptedException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
if (checker.result() != null) {
|
|
throw checker.result();
|
|
}
|
|
} finally { // log all the messages to STDOUT
|
|
System.out.println(LOGGER.toString());
|
|
}
|
|
System.out.println("Test passed.");
|
|
}
|
|
|
|
private static ThreadInfo findOwnerInfo(ThreadInfo[] infos, String lock)
|
|
throws Exception {
|
|
ThreadInfo ownerInfo = null;
|
|
for (ThreadInfo info : infos) {
|
|
if (info == null) {
|
|
continue; // Missing thread, e.g. completed. Ignore.
|
|
}
|
|
String blockedLock = info.getLockName();
|
|
if (lock.equals(blockedLock)) {
|
|
long threadId = info.getLockOwnerId();
|
|
if (threadId == -1) {
|
|
throw new RuntimeException("TEST FAILED: " +
|
|
lock + " expected to have owner");
|
|
}
|
|
for (ThreadInfo info1 : infos) {
|
|
if (info1 == null) {
|
|
continue; // Missing thread, e.g. completed. Ignore.
|
|
}
|
|
if (info1.getThreadId() == threadId) {
|
|
ownerInfo = info1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ownerInfo;
|
|
}
|
|
private static void findThreadsBlockedOn(Object o, long[] expectedThreads)
|
|
throws Exception {
|
|
String lock = getLockName(o);
|
|
// Check with ThreadInfo with no stack trace (i.e. no safepoint)
|
|
ThreadInfo[] allThreadInfos = TM.getThreadInfo(TM.getAllThreadIds());
|
|
doCheck(allThreadInfos, lock, expectedThreads);
|
|
|
|
// Check with ThreadInfo with stack trace
|
|
allThreadInfos = TM.getThreadInfo(TM.getAllThreadIds(), 1);
|
|
doCheck(allThreadInfos, lock, expectedThreads);
|
|
}
|
|
|
|
private static void doCheck(ThreadInfo[] infos, String lock, long[] expectedThreads)
|
|
throws Exception {
|
|
ThreadInfo ownerInfo = null;
|
|
// Find the thread who is blocking on lock
|
|
for (ThreadInfo info : infos) {
|
|
if (info == null) {
|
|
continue; // Missing thread, e.g. completed. Ignore.
|
|
}
|
|
String blockedLock = info.getLockName();
|
|
if (lock.equals(blockedLock)) {
|
|
log("%s blocked on %s", info.getThreadName(), blockedLock);
|
|
ownerInfo = info;
|
|
}
|
|
}
|
|
if (ownerInfo == null) {
|
|
throw new RuntimeException("TEST FAILED: " +
|
|
"Can't retrieve ThreadInfo for the blocked thread");
|
|
}
|
|
|
|
// Follow chain of locks:
|
|
long[] threads = new long[10];
|
|
int count = 0;
|
|
threads[count++] = ownerInfo.getThreadId();
|
|
while (ownerInfo.getThreadState() == Thread.State.BLOCKED) {
|
|
ownerInfo = findOwnerInfo(infos, lock);
|
|
log("ownerInfo = %s", ownerInfo);
|
|
if (ownerInfo.getThreadName().contains("ForkJoinPool")) {
|
|
// Ignore e.g. "ForkJoinPool-1-worker-1" waiting on a VirtualThread
|
|
log ("skipping %s", ownerInfo);
|
|
lock = ownerInfo.getLockName();
|
|
continue;
|
|
}
|
|
threads[count++] = ownerInfo.getThreadId();
|
|
log(" Owner = %s id = %d",
|
|
ownerInfo.getThreadName(),
|
|
ownerInfo.getThreadId()
|
|
);
|
|
lock = ownerInfo.getLockName();
|
|
log("%s Id = %d blocked on %s",
|
|
ownerInfo.getThreadName(),
|
|
ownerInfo.getThreadId(),
|
|
lock
|
|
);
|
|
}
|
|
log("");
|
|
|
|
if (count != expectedThreads.length) {
|
|
throw new RuntimeException("TEST FAILED: " +
|
|
"Expected chain of threads not matched; current count =" + count);
|
|
}
|
|
int failures = 0;
|
|
for (int i = 0; i < count; i++) {
|
|
if (threads[i] != expectedThreads[i]) {
|
|
log("TEST FAILED: Unexpected thread in the chain %s expected to be %s",
|
|
threads[i],
|
|
expectedThreads[i]
|
|
);
|
|
failures++;
|
|
}
|
|
}
|
|
if (failures > 0) {
|
|
throw new RuntimeException("TEST FAILED: " + failures + " unexpected thread(s).");
|
|
}
|
|
}
|
|
|
|
private static void log(String format, Object ... args) {
|
|
LOGGER.log(format + "%n", args);
|
|
}
|
|
}
|