blob: 423ffa03d313248b33c456087cde65a5a74c4483 [file]
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
package jdk.internal.misc;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.time.Duration;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.StructureViolationException;
import java.util.concurrent.locks.LockSupport;
import java.util.stream.Stream;
import jdk.internal.access.JavaLangAccess;
import jdk.internal.access.SharedSecrets;
import jdk.internal.invoke.MhUtil;
import jdk.internal.vm.ScopedValueContainer;
import jdk.internal.vm.ThreadContainer;
import jdk.internal.vm.ThreadContainers;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
/**
* A grouping of threads that typically run closely related tasks. Threads started
* in a flock remain in the flock until they terminate.
*
* <p> ThreadFlock defines the {@link #open(String) open} method to open a new flock,
* the {@link #start(Thread) start} method to start a thread in the flock, and the
* {@link #close() close} method to close the flock. The {@code close} waits for all
* threads in the flock to finish. The {@link #awaitAll() awaitAll} method may be used
* to wait for all threads to finish without closing the flock. The {@link #wakeup()}
* method will cause {@code awaitAll} method to complete early, which can be used to
* support cancellation in higher-level APIs. ThreadFlock also defines the {@link
* #shutdown() shutdown} method to prevent new threads from starting while allowing
* existing threads in the flock to continue.
*
* <p> Thread flocks are intended to be used in a <em>structured manner</em>. The
* thread that opens a new flock is the {@link #owner() owner}. The owner closes the
* flock when done, failure to do so may result in a resource leak. The {@code open}
* and {@code close} should be matched to avoid closing an <em>enclosing</em> flock
* while a <em>nested</em> flock is open. A ThreadFlock can be used with the
* try-with-resources construct if required but more likely, the close method of a
* higher-level API that implements {@link AutoCloseable} will close the flock.
*
* <p> Thread flocks are conceptually nodes in a tree. A thread {@code T} started in
* flock "A" may itself open a new flock "B", implicitly forming a tree where flock
* "A" is the parent of flock "B". When nested, say where thread {@code T} opens
* flock "B" and then invokes a method that opens flock "C", then the enclosing
* flock "B" is conceptually the parent of the nested flock "C". ThreadFlock does
* not define APIs that exposes the tree structure. It does define the {@link
* #containsThread(Thread) containsThread} method to test if a flock contains a
* thread, a test that is equivalent to testing membership of flocks in the tree.
* The {@code start} and {@code shutdown} methods are confined to the flock
* owner or threads contained in the flock. The confinement check is equivalent to
* invoking the {@code containsThread} method to test if the caller is contained
* in the flock.
*
* <p> Unless otherwise specified, passing a {@code null} argument to a method
* in this class will cause a {@link NullPointerException} to be thrown.
*/
public class ThreadFlock implements AutoCloseable {
private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
private static final VarHandle THREAD_COUNT;
private static final VarHandle PERMIT;
static {
MethodHandles.Lookup l = MethodHandles.lookup();
THREAD_COUNT = MhUtil.findVarHandle(l, "threadCount", int.class);
PERMIT = MhUtil.findVarHandle(l, "permit", boolean.class);
}
private final Set<Thread> threads = ConcurrentHashMap.newKeySet();
// thread count, need to re-examine contention once API is stable
private volatile int threadCount;
private final String name;
private final ScopedValueContainer.BindingsSnapshot scopedValueBindings;
private final ThreadContainerImpl container; // encapsulate for now
// state
private volatile boolean shutdown;
private volatile boolean closed;
// set by wakeup, cleared by awaitAll
private volatile boolean permit;
ThreadFlock(String name) {
this.name = name;
this.scopedValueBindings = ScopedValueContainer.captureBindings();
this.container = new ThreadContainerImpl(this);
}
private long threadCount() {
return threadCount;
}
private ScopedValueContainer.BindingsSnapshot scopedValueBindings() {
return scopedValueBindings;
}
private void incrementThreadCount() {
THREAD_COUNT.getAndAdd(this, 1);
}
/**
* Decrement the thread count. Unpark the owner if the count goes to zero.
*/
private void decrementThreadCount() {
int count = (int) THREAD_COUNT.getAndAdd(this, -1) - 1;
// signal owner when the count goes to zero
if (count == 0) {
LockSupport.unpark(owner());
}
}
/**
* Invoked on the parent thread when starting {@code thread}.
*/
private void onStart(Thread thread) {
incrementThreadCount();
boolean done = false;
try {
boolean added = threads.add(thread);
assert added;
if (shutdown)
throw new IllegalStateException("Shutdown");
done = true;
} finally {
if (!done) {
threads.remove(thread);
decrementThreadCount();
}
}
}
/**
* Invoked on the terminating thread or the parent thread when starting
* {@code thread} failed. This method is only called if onStart succeeded.
*/
private void onExit(Thread thread) {
boolean removed = threads.remove(thread);
assert removed;
decrementThreadCount();
}
/**
* Clear wakeup permit.
*/
private void clearPermit() {
if (permit)
permit = false;
}
/**
* Sets the wakeup permit to the given value, returning the previous value.
*/
private boolean getAndSetPermit(boolean newValue) {
if (permit != newValue) {
return (boolean) PERMIT.getAndSet(this, newValue);
} else {
return newValue;
}
}
/**
* Throws WrongThreadException if the current thread is not the owner.
*/
private void ensureOwner() {
if (Thread.currentThread() != owner())
throw new WrongThreadException("Current thread not owner");
}
/**
* Throws WrongThreadException if the current thread is not the owner
* or a thread contained in the flock.
*/
private void ensureOwnerOrContainsThread() {
Thread currentThread = Thread.currentThread();
if (currentThread != owner() && !containsThread(currentThread))
throw new WrongThreadException("Current thread not owner or thread in flock");
}
/**
* Opens a new thread flock. The flock is owned by the current thread. It can be
* named to aid debugging.
*
* <p> This method captures the current thread's {@linkplain ScopedValue scoped value}
* bindings for inheritance by threads created in the flock.
*
* <p> For the purposes of containment, monitoring, and debugging, the parent
* of the new flock is determined as follows:
* <ul>
* <li> If the current thread is the owner of open flocks then the most recently
* created, and open, flock is the parent of the new flock. In other words, the
* <em>enclosing flock</em> is the parent.
* <li> If the current thread is not the owner of any open flocks then the
* parent of the new flock is the current thread's flock. If the current thread
* was not started in a flock then the new flock does not have a parent.
* </ul>
*
* @param name the name of the flock, can be null
* @return a new thread flock
*/
public static ThreadFlock open(String name) {
var flock = new ThreadFlock(name);
flock.container.push();
return flock;
}
/**
* {@return the name of this flock or {@code null} if unnamed}
*/
public String name() {
return name;
}
/**
* {@return the owner of this flock}
*/
public Thread owner() {
return container.owner();
}
/**
* Starts the given unstarted thread in this flock.
*
* <p> The thread is started with the scoped value bindings that were captured
* when opening the flock. The bindings must match the current thread's bindings.
*
* <p> This method may only be invoked by the flock owner or threads {@linkplain
* #containsThread(Thread) contained} in the flock.
*
* @param thread the unstarted thread
* @return the thread, started
* @throws IllegalStateException if this flock is shutdown or closed
* @throws IllegalThreadStateException if the given thread was already started
* @throws WrongThreadException if the current thread is not the owner or a thread
* contained in the flock
* @throws StructureViolationException if the current scoped value bindings are
* not the same as when the flock was created
*/
public Thread start(Thread thread) {
ensureOwnerOrContainsThread();
JLA.start(thread, container);
return thread;
}
/**
* Shutdown this flock so that no new threads can be started, existing threads
* in the flock will continue to run. This method is a no-op if the flock is
* already shutdown or closed.
*/
public void shutdown() {
if (!shutdown) {
shutdown = true;
}
}
/**
* Wait for all threads in the flock to finish executing their tasks. This method
* waits until all threads finish, the {@link #wakeup() wakeup} method is invoked,
* or the current thread is interrupted.
*
* <p> This method may only be invoked by the flock owner. The method trivially
* returns true when the flock is closed.
*
* <p> This method clears the effect of any previous invocations of the
* {@code wakeup} method.
*
* @return true if there are no threads in the flock, false if wakeup was invoked
* and there are unfinished threads
* @throws InterruptedException if interrupted while waiting
* @throws WrongThreadException if invoked by a thread that is not the owner
*/
public boolean awaitAll() throws InterruptedException {
ensureOwner();
if (getAndSetPermit(false))
return (threadCount == 0);
while (threadCount > 0 && !permit) {
LockSupport.park();
if (Thread.interrupted())
throw new InterruptedException();
}
clearPermit();
return (threadCount == 0);
}
/**
* Wait, up to the given waiting timeout, for all threads in the flock to finish
* executing their tasks. This method waits until all threads finish, the {@link
* #wakeup() wakeup} method is invoked, the current thread is interrupted, or
* the timeout expires.
*
* <p> This method may only be invoked by the flock owner. The method trivially
* returns true when the flock is closed.
*
* <p> This method clears the effect of any previous invocations of the {@code wakeup}
* method.
*
* @param timeout the maximum duration to wait
* @return true if there are no threads in the flock, false if wakeup was invoked
* and there are unfinished threads
* @throws InterruptedException if interrupted while waiting
* @throws TimeoutException if the wait timed out
* @throws WrongThreadException if invoked by a thread that is not the owner
*/
public boolean awaitAll(Duration timeout)
throws InterruptedException, TimeoutException {
Objects.requireNonNull(timeout);
ensureOwner();
if (getAndSetPermit(false))
return (threadCount == 0);
long startNanos = System.nanoTime();
long nanos = NANOSECONDS.convert(timeout);
long remainingNanos = nanos;
while (threadCount > 0 && remainingNanos > 0 && !permit) {
LockSupport.parkNanos(remainingNanos);
if (Thread.interrupted())
throw new InterruptedException();
remainingNanos = nanos - (System.nanoTime() - startNanos);
}
boolean done = (threadCount == 0);
if (!done && remainingNanos <= 0 && !permit) {
throw new TimeoutException();
} else {
clearPermit();
return done;
}
}
/**
* Causes the call to {@link #awaitAll()} or {@link #awaitAll(Duration)} by the
* {@linkplain #owner() owner} to return immediately.
*
* <p> If the owner is blocked in {@code awaitAll} then it will return immediately.
* If the owner is not blocked in {@code awaitAll} then its next call to wait
* will return immediately. The method does nothing when the flock is closed.
*/
public void wakeup() {
if (!getAndSetPermit(true) && Thread.currentThread() != owner()) {
LockSupport.unpark(owner());
}
}
/**
* Closes this flock. This method first shuts down the flock to prevent
* new threads from starting. It then waits for the threads in the flock
* to finish executing their tasks. In other words, this method blocks until
* all threads in the flock finish.
*
* <p> This method may only be invoked by the flock owner.
*
* <p> If interrupted then this method continues to wait until all threads
* finish, before completing with the interrupt status set.
*
* <p> A ThreadFlock is intended to be used in a <em>structured manner</em>. If
* this method is called to close a flock before nested flocks are closed then it
* closes the nested flocks (in the reverse order that they were created in),
* closes this flock, and then throws {@code StructureViolationException}.
* Similarly, if this method is called to close a thread flock while executing with
* scoped value bindings, and the thread flock was created before the scoped values
* were bound, then {@code StructureViolationException} is thrown after closing the
* thread flock.
*
* @throws WrongThreadException if invoked by a thread that is not the owner
* @throws StructureViolationException if a structure violation was detected
*/
public void close() {
ensureOwner();
if (closed)
return;
// shutdown, if not already shutdown
if (!shutdown)
shutdown = true;
// wait for threads to finish
boolean interrupted = false;
try {
while (threadCount > 0) {
LockSupport.park();
if (Thread.interrupted()) {
interrupted = true;
}
}
} finally {
try {
container.close(); // may throw
} finally {
closed = true;
if (interrupted) Thread.currentThread().interrupt();
}
}
}
/**
* {@return true if the flock has been {@linkplain #shutdown() shut down}}
*/
public boolean isShutdown() {
return shutdown;
}
/**
* {@return true if the flock has been {@linkplain #close() closed}}
*/
public boolean isClosed() {
return closed;
}
/**
* {@return a stream of the threads in this flock}
* The elements of the stream are threads that were started in this flock
* but have not terminated. The stream will reflect the set of threads in the
* flock at some point at or since the creation of the stream. It may or may
* not reflect changes to the set of threads subsequent to creation of the
* stream.
*/
public Stream<Thread> threads() {
return threads.stream();
}
/**
* Tests if this flock contains the given thread. This method returns {@code true}
* if the thread was started in this flock and has not finished. If the thread
* is not in this flock then it tests if the thread is in flocks owned by threads
* in this flock, essentially equivalent to invoking {@code containsThread} method
* on all flocks owned by the threads in this flock.
*
* @param thread the thread
* @return true if this flock contains the thread
*/
public boolean containsThread(Thread thread) {
var c = JLA.threadContainer(thread);
if (c == this.container)
return true;
if (c != null && c != ThreadContainers.root()) {
var parent = c.parent();
while (parent != null) {
if (parent == this.container)
return true;
parent = parent.parent();
}
}
return false;
}
@Override
public String toString() {
String id = Objects.toIdentityString(this);
if (name != null) {
return name + "/" + id;
} else {
return id;
}
}
/**
* A ThreadContainer backed by a ThreadFlock.
*/
private static class ThreadContainerImpl extends ThreadContainer {
private final ThreadFlock flock;
private volatile Object key;
private boolean closing;
ThreadContainerImpl(ThreadFlock flock) {
super(/*shared*/ false);
this.flock = flock;
}
@Override
public ThreadContainerImpl push() {
// Virtual threads in the root containers may not be tracked so need
// to register container to ensure that it is found
if (!ThreadContainers.trackAllThreads()) {
Thread thread = Thread.currentThread();
if (thread.isVirtual()
&& JLA.threadContainer(thread) == ThreadContainers.root()) {
this.key = ThreadContainers.registerContainer(this);
}
}
super.push();
return this;
}
/**
* Invoked by ThreadFlock.close when closing the flock. This method pops the
* container from the current thread's scope stack.
*/
void close() {
assert Thread.currentThread() == owner();
if (!closing) {
closing = true;
boolean atTop = popForcefully(); // may block
Object key = this.key;
if (key != null)
ThreadContainers.deregisterContainer(key);
if (!atTop)
throw new StructureViolationException();
}
}
/**
* Invoked when an enclosing scope is closing. Invokes ThreadFlock.close to
* close the flock. This method does not pop the container from the current
* thread's scope stack.
*/
@Override
protected boolean tryClose() {
assert Thread.currentThread() == owner();
if (!closing) {
closing = true;
flock.close();
Object key = this.key;
if (key != null)
ThreadContainers.deregisterContainer(key);
return true;
} else {
assert false : "Should not get there";
return false;
}
}
@Override
public String name() {
return flock.name();
}
@Override
public long threadCount() {
return flock.threadCount();
}
@Override
public Stream<Thread> threads() {
return flock.threads().filter(Thread::isAlive);
}
@Override
public void onStart(Thread thread) {
flock.onStart(thread);
}
@Override
public void onExit(Thread thread) {
flock.onExit(thread);
}
@Override
public ScopedValueContainer.BindingsSnapshot scopedValueBindings() {
return flock.scopedValueBindings();
}
}
}