com.diffplug.common.util.concurrent.CycleDetectingLockFactory Maven / Gradle / Ivy
Show all versions of durian-concurrent Show documentation
/*
* Original Guava code is copyright (C) 2015 The Guava Authors.
* Modifications from Guava are copyright (C) 2016 DiffPlug.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.common.util.concurrent;
import static com.diffplug.common.base.Preconditions.checkNotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import com.google.j2objc.annotations.Weak;
import com.diffplug.common.annotations.Beta;
import com.diffplug.common.annotations.VisibleForTesting;
import com.diffplug.common.base.MoreObjects;
import com.diffplug.common.base.Preconditions;
import com.diffplug.common.collect.ImmutableSet;
import com.diffplug.common.collect.Lists;
import com.diffplug.common.collect.MapMaker;
import com.diffplug.common.collect.Maps;
import com.diffplug.common.collect.Sets;
/**
* The {@code CycleDetectingLockFactory} creates {@link ReentrantLock} instances and
* {@link ReentrantReadWriteLock} instances that detect potential deadlock by checking
* for cycles in lock acquisition order.
*
* Potential deadlocks detected when calling the {@code lock()},
* {@code lockInterruptibly()}, or {@code tryLock()} methods will result in the
* execution of the {@link Policy} specified when creating the factory. The
* currently available policies are:
*
* - DISABLED
*
- WARN
*
- THROW
*
* The locks created by a factory instance will detect lock acquisition cycles
* with locks created by other {@code CycleDetectingLockFactory} instances
* (except those with {@code Policy.DISABLED}). A lock's behavior when a cycle
* is detected, however, is defined by the {@code Policy} of the factory that
* created it. This allows detection of cycles across components while
* delegating control over lock behavior to individual components.
*
* Applications are encouraged to use a {@code CycleDetectingLockFactory} to
* create any locks for which external/unmanaged code is executed while the lock
* is held. (See caveats under Performance).
*
* Cycle Detection
*
* Deadlocks can arise when locks are acquired in an order that forms a cycle.
* In a simple example involving two locks and two threads, deadlock occurs
* when one thread acquires Lock A, and then Lock B, while another thread
* acquires Lock B, and then Lock A:
*
* Thread1: acquire(LockA) --X acquire(LockB)
* Thread2: acquire(LockB) --X acquire(LockA)
*
* Neither thread will progress because each is waiting for the other. In more
* complex applications, cycles can arise from interactions among more than 2
* locks:
*
* Thread1: acquire(LockA) --X acquire(LockB)
* Thread2: acquire(LockB) --X acquire(LockC)
* ...
* ThreadN: acquire(LockN) --X acquire(LockA)
*
* The implementation detects cycles by constructing a directed graph in which
* each lock represents a node and each edge represents an acquisition ordering
* between two locks.
*
* - Each lock adds (and removes) itself to/from a ThreadLocal Set of acquired
* locks when the Thread acquires its first hold (and releases its last
* remaining hold).
*
- Before the lock is acquired, the lock is checked against the current set
* of acquired locks---to each of the acquired locks, an edge from the
* soon-to-be-acquired lock is either verified or created.
*
- If a new edge needs to be created, the outgoing edges of the acquired
* locks are traversed to check for a cycle that reaches the lock to be
* acquired. If no cycle is detected, a new "safe" edge is created.
*
- If a cycle is detected, an "unsafe" (cyclic) edge is created to represent
* a potential deadlock situation, and the appropriate Policy is executed.
*
* Note that detection of potential deadlock does not necessarily indicate that
* deadlock will happen, as it is possible that higher level application logic
* prevents the cyclic lock acquisition from occurring. One example of a false
* positive is:
*
* LockA -> LockB -> LockC
* LockA -> LockC -> LockB
*
*
* ReadWriteLocks
*
* While {@code ReadWriteLock} instances have different properties and can form cycles
* without potential deadlock, this class treats {@code ReadWriteLock} instances as
* equivalent to traditional exclusive locks. Although this increases the false
* positives that the locks detect (i.e. cycles that will not actually result in
* deadlock), it simplifies the algorithm and implementation considerably. The
* assumption is that a user of this factory wishes to eliminate any cyclic
* acquisition ordering.
*
* Explicit Lock Acquisition Ordering
*
* The {@link CycleDetectingLockFactory.WithExplicitOrdering} class can be used
* to enforce an application-specific ordering in addition to performing general
* cycle detection.
*
* Garbage Collection
*
* In order to allow proper garbage collection of unused locks, the edges of
* the lock graph are weak references.
*
* Performance
*
* The extra bookkeeping done by cycle detecting locks comes at some cost to
* performance. Benchmarks (as of December 2011) show that:
*
*
* - for an unnested {@code lock()} and {@code unlock()}, a cycle detecting
* lock takes 38ns as opposed to the 24ns taken by a plain lock.
*
- for nested locking, the cost increases with the depth of the nesting:
*
* - 2 levels: average of 64ns per lock()/unlock()
*
- 3 levels: average of 77ns per lock()/unlock()
*
- 4 levels: average of 99ns per lock()/unlock()
*
- 5 levels: average of 103ns per lock()/unlock()
*
- 10 levels: average of 184ns per lock()/unlock()
*
- 20 levels: average of 393ns per lock()/unlock()
*
*
*
* As such, the CycleDetectingLockFactory may not be suitable for
* performance-critical applications which involve tightly-looped or
* deeply-nested locking algorithms.
*
* @author Darick Tong
* @since 13.0
*/
@Beta
@ThreadSafe
public class CycleDetectingLockFactory {
/**
* Encapsulates the action to be taken when a potential deadlock is
* encountered. Clients can use one of the predefined {@link Policies} or
* specify a custom implementation. Implementations must be thread-safe.
*
* @since 13.0
*/
@Beta
@ThreadSafe
public interface Policy {
/**
* Called when a potential deadlock is encountered. Implementations can
* throw the given {@code exception} and/or execute other desired logic.
*
* Note that the method will be called even upon an invocation of
* {@code tryLock()}. Although {@code tryLock()} technically recovers from
* deadlock by eventually timing out, this behavior is chosen based on the
* assumption that it is the application's wish to prohibit any cyclical
* lock acquisitions.
*/
void handlePotentialDeadlock(PotentialDeadlockException exception);
}
/**
* Pre-defined {@link Policy} implementations.
*
* @since 13.0
*/
@Beta
public enum Policies implements Policy {
/**
* When potential deadlock is detected, this policy results in the throwing
* of the {@code PotentialDeadlockException} indicating the potential
* deadlock, which includes stack traces illustrating the cycle in lock
* acquisition order.
*/
THROW {
@Override
public void handlePotentialDeadlock(PotentialDeadlockException e) {
throw e;
}
},
/**
* When potential deadlock is detected, this policy results in the logging
* of a {@link Level#SEVERE} message indicating the potential deadlock,
* which includes stack traces illustrating the cycle in lock acquisition
* order.
*/
WARN {
@Override
public void handlePotentialDeadlock(PotentialDeadlockException e) {
logger.log(Level.SEVERE, "Detected potential deadlock", e);
}
},
/**
* Disables cycle detection. This option causes the factory to return
* unmodified lock implementations provided by the JDK, and is provided to
* allow applications to easily parameterize when cycle detection is
* enabled.
*
* Note that locks created by a factory with this policy will not
* participate the cycle detection performed by locks created by other
* factories.
*/
DISABLED {
@Override
public void handlePotentialDeadlock(PotentialDeadlockException e) {}
};
}
/**
* Creates a new factory with the specified policy.
*/
public static CycleDetectingLockFactory newInstance(Policy policy) {
return new CycleDetectingLockFactory(policy);
}
/**
* Equivalent to {@code newReentrantLock(lockName, false)}.
*/
public ReentrantLock newReentrantLock(String lockName) {
return newReentrantLock(lockName, false);
}
/**
* Creates a {@link ReentrantLock} with the given fairness policy. The
* {@code lockName} is used in the warning or exception output to help
* identify the locks involved in the detected deadlock.
*/
public ReentrantLock newReentrantLock(String lockName, boolean fair) {
return policy == Policies.DISABLED ? new ReentrantLock(fair)
: new CycleDetectingReentrantLock(
new LockGraphNode(lockName), fair);
}
/**
* Equivalent to {@code newReentrantReadWriteLock(lockName, false)}.
*/
public ReentrantReadWriteLock newReentrantReadWriteLock(String lockName) {
return newReentrantReadWriteLock(lockName, false);
}
/**
* Creates a {@link ReentrantReadWriteLock} with the given fairness policy.
* The {@code lockName} is used in the warning or exception output to help
* identify the locks involved in the detected deadlock.
*/
public ReentrantReadWriteLock newReentrantReadWriteLock(
String lockName, boolean fair) {
return policy == Policies.DISABLED ? new ReentrantReadWriteLock(fair)
: new CycleDetectingReentrantReadWriteLock(
new LockGraphNode(lockName), fair);
}
// A static mapping from an Enum type to its set of LockGraphNodes.
private static final ConcurrentMap, Map extends Enum, LockGraphNode>> lockGraphNodesPerType = new MapMaker().weakKeys().makeMap();
/**
* Creates a {@code CycleDetectingLockFactory.WithExplicitOrdering}.
*/
public static > WithExplicitOrdering newInstanceWithExplicitOrdering(Class enumClass, Policy policy) {
// createNodes maps each enumClass to a Map with the corresponding enum key
// type.
checkNotNull(enumClass);
checkNotNull(policy);
@SuppressWarnings("unchecked")
Map lockGraphNodes = (Map) getOrCreateNodes(enumClass);
return new WithExplicitOrdering(policy, lockGraphNodes);
}
private static Map extends Enum, LockGraphNode> getOrCreateNodes(
Class extends Enum> clazz) {
Map extends Enum, LockGraphNode> existing = lockGraphNodesPerType.get(clazz);
if (existing != null) {
return existing;
}
Map extends Enum, LockGraphNode> created = createNodes(clazz);
existing = lockGraphNodesPerType.putIfAbsent(clazz, created);
return MoreObjects.firstNonNull(existing, created);
}
/**
* For a given Enum type, creates an immutable map from each of the Enum's
* values to a corresponding LockGraphNode, with the
* {@code allowedPriorLocks} and {@code disallowedPriorLocks} prepopulated
* with nodes according to the natural ordering of the associated Enum values.
*/
@VisibleForTesting
static > Map createNodes(Class clazz) {
EnumMap map = Maps.newEnumMap(clazz);
E[] keys = clazz.getEnumConstants();
final int numKeys = keys.length;
ArrayList nodes = Lists.newArrayListWithCapacity(numKeys);
// Create a LockGraphNode for each enum value.
for (E key : keys) {
LockGraphNode node = new LockGraphNode(getLockName(key));
nodes.add(node);
map.put(key, node);
}
// Pre-populate all allowedPriorLocks with nodes of smaller ordinal.
for (int i = 1; i < numKeys; i++) {
nodes.get(i).checkAcquiredLocks(Policies.THROW, nodes.subList(0, i));
}
// Pre-populate all disallowedPriorLocks with nodes of larger ordinal.
for (int i = 0; i < numKeys - 1; i++) {
nodes.get(i).checkAcquiredLocks(
Policies.DISABLED, nodes.subList(i + 1, numKeys));
}
return Collections.unmodifiableMap(map);
}
/**
* For the given Enum value {@code rank}, returns the value's
* {@code "EnumClass.name"}, which is used in exception and warning
* output.
*/
private static String getLockName(Enum> rank) {
return rank.getDeclaringClass().getSimpleName() + "." + rank.name();
}
/**
* A {@code CycleDetectingLockFactory.WithExplicitOrdering} provides the
* additional enforcement of an application-specified ordering of lock
* acquisitions. The application defines the allowed ordering with an
* {@code Enum} whose values each correspond to a lock type. The order in
* which the values are declared dictates the allowed order of lock
* acquisition. In other words, locks corresponding to smaller values of
* {@link Enum#ordinal()} should only be acquired before locks with larger
* ordinals. Example:
*
*
{@code
* enum MyLockOrder {
* FIRST, SECOND, THIRD;
* }
*
* CycleDetectingLockFactory.WithExplicitOrdering factory =
* CycleDetectingLockFactory.newInstanceWithExplicitOrdering(Policies.THROW);
*
* Lock lock1 = factory.newReentrantLock(MyLockOrder.FIRST);
* Lock lock2 = factory.newReentrantLock(MyLockOrder.SECOND);
* Lock lock3 = factory.newReentrantLock(MyLockOrder.THIRD);
*
* lock1.lock();
* lock3.lock();
* lock2.lock(); // will throw an IllegalStateException}
*
* As with all locks created by instances of {@code CycleDetectingLockFactory}
* explicitly ordered locks participate in general cycle detection with all
* other cycle detecting locks, and a lock's behavior when detecting a cyclic
* lock acquisition is defined by the {@code Policy} of the factory that
* created it.
*
*
Note, however, that although multiple locks can be created for a given Enum
* value, whether it be through separate factory instances or through multiple
* calls to the same factory, attempting to acquire multiple locks with the
* same Enum value (within the same thread) will result in an
* IllegalStateException regardless of the factory's policy. For example:
*
*
{@code
* CycleDetectingLockFactory.WithExplicitOrdering factory1 =
* CycleDetectingLockFactory.newInstanceWithExplicitOrdering(...);
* CycleDetectingLockFactory.WithExplicitOrdering factory2 =
* CycleDetectingLockFactory.newInstanceWithExplicitOrdering(...);
*
* Lock lockA = factory1.newReentrantLock(MyLockOrder.FIRST);
* Lock lockB = factory1.newReentrantLock(MyLockOrder.FIRST);
* Lock lockC = factory2.newReentrantLock(MyLockOrder.FIRST);
*
* lockA.lock();
*
* lockB.lock(); // will throw an IllegalStateException
* lockC.lock(); // will throw an IllegalStateException
*
* lockA.lock(); // reentrant acquisition is okay}
*
* It is the responsibility of the application to ensure that multiple lock
* instances with the same rank are never acquired in the same thread.
*
* @param The Enum type representing the explicit lock ordering.
* @since 13.0
*/
@Beta
public static final class WithExplicitOrdering>
extends CycleDetectingLockFactory {
private final Map lockGraphNodes;
@VisibleForTesting
WithExplicitOrdering(
Policy policy, Map lockGraphNodes) {
super(policy);
this.lockGraphNodes = lockGraphNodes;
}
/**
* Equivalent to {@code newReentrantLock(rank, false)}.
*/
public ReentrantLock newReentrantLock(E rank) {
return newReentrantLock(rank, false);
}
/**
* Creates a {@link ReentrantLock} with the given fairness policy and rank.
* The values returned by {@link Enum#getDeclaringClass()} and
* {@link Enum#name()} are used to describe the lock in warning or
* exception output.
*
* @throws IllegalStateException If the factory has already created a
* {@code Lock} with the specified rank.
*/
public ReentrantLock newReentrantLock(E rank, boolean fair) {
return policy == Policies.DISABLED ? new ReentrantLock(fair)
: new CycleDetectingReentrantLock(lockGraphNodes.get(rank), fair);
}
/**
* Equivalent to {@code newReentrantReadWriteLock(rank, false)}.
*/
public ReentrantReadWriteLock newReentrantReadWriteLock(E rank) {
return newReentrantReadWriteLock(rank, false);
}
/**
* Creates a {@link ReentrantReadWriteLock} with the given fairness policy
* and rank. The values returned by {@link Enum#getDeclaringClass()} and
* {@link Enum#name()} are used to describe the lock in warning or exception
* output.
*
* @throws IllegalStateException If the factory has already created a
* {@code Lock} with the specified rank.
*/
public ReentrantReadWriteLock newReentrantReadWriteLock(
E rank, boolean fair) {
return policy == Policies.DISABLED ? new ReentrantReadWriteLock(fair)
: new CycleDetectingReentrantReadWriteLock(
lockGraphNodes.get(rank), fair);
}
}
//////// Implementation /////////
private static final Logger logger = Logger.getLogger(
CycleDetectingLockFactory.class.getName());
final Policy policy;
private CycleDetectingLockFactory(Policy policy) {
this.policy = checkNotNull(policy);
}
/**
* Tracks the currently acquired locks for each Thread, kept up to date by
* calls to {@link #aboutToAcquire(CycleDetectingLock)} and
* {@link #lockStateChanged(CycleDetectingLock)}.
*/
// This is logically a Set, but an ArrayList is used to minimize the amount
// of allocation done on lock()/unlock().
private static final ThreadLocal> acquiredLocks = new ThreadLocal>() {
@Override
protected ArrayList initialValue() {
return Lists. newArrayListWithCapacity(3);
}
};
/**
* A Throwable used to record a stack trace that illustrates an example of
* a specific lock acquisition ordering. The top of the stack trace is
* truncated such that it starts with the acquisition of the lock in
* question, e.g.
*
*
* com...ExampleStackTrace: LockB -> LockC
* at com...CycleDetectingReentrantLock.lock(CycleDetectingLockFactory.java:443)
* at ...
* at ...
* at com...MyClass.someMethodThatAcquiresLockB(MyClass.java:123)
*
*/
private static class ExampleStackTrace extends IllegalStateException {
static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
static final Set EXCLUDED_CLASS_NAMES = ImmutableSet.of(
CycleDetectingLockFactory.class.getName(),
ExampleStackTrace.class.getName(),
LockGraphNode.class.getName());
ExampleStackTrace(LockGraphNode node1, LockGraphNode node2) {
super(node1.getLockName() + " -> " + node2.getLockName());
StackTraceElement[] origStackTrace = getStackTrace();
for (int i = 0, n = origStackTrace.length; i < n; i++) {
if (WithExplicitOrdering.class.getName().equals(
origStackTrace[i].getClassName())) {
// For pre-populated disallowedPriorLocks edges, omit the stack trace.
setStackTrace(EMPTY_STACK_TRACE);
break;
}
if (!EXCLUDED_CLASS_NAMES.contains(origStackTrace[i].getClassName())) {
setStackTrace(Arrays.copyOfRange(origStackTrace, i, n));
break;
}
}
}
}
/**
* Represents a detected cycle in lock acquisition ordering. The exception
* includes a causal chain of {@code ExampleStackTrace} instances to illustrate the
* cycle, e.g.
*
*
* com....PotentialDeadlockException: Potential Deadlock from LockC -> ReadWriteA
* at ...
* at ...
* Caused by: com...ExampleStackTrace: LockB -> LockC
* at ...
* at ...
* Caused by: com...ExampleStackTrace: ReadWriteA -> LockB
* at ...
* at ...
*
*
* Instances are logged for the {@code Policies.WARN}, and thrown for
* {@code Policies.THROW}.
*
* @since 13.0
*/
@Beta
public static final class PotentialDeadlockException
extends ExampleStackTrace {
private final ExampleStackTrace conflictingStackTrace;
private PotentialDeadlockException(
LockGraphNode node1,
LockGraphNode node2,
ExampleStackTrace conflictingStackTrace) {
super(node1, node2);
this.conflictingStackTrace = conflictingStackTrace;
initCause(conflictingStackTrace);
}
public ExampleStackTrace getConflictingStackTrace() {
return conflictingStackTrace;
}
/**
* Appends the chain of messages from the {@code conflictingStackTrace} to
* the original {@code message}.
*/
@Override
public String getMessage() {
StringBuilder message = new StringBuilder(super.getMessage());
for (Throwable t = conflictingStackTrace; t != null; t = t.getCause()) {
message.append(", ").append(t.getMessage());
}
return message.toString();
}
}
/**
* Internal Lock implementations implement the {@code CycleDetectingLock}
* interface, allowing the detection logic to treat all locks in the same
* manner.
*/
private interface CycleDetectingLock {
/** @return the {@link LockGraphNode} associated with this lock. */
LockGraphNode getLockGraphNode();
/** @return {@code true} if the current thread has acquired this lock. */
boolean isAcquiredByCurrentThread();
}
/**
* A {@code LockGraphNode} associated with each lock instance keeps track of
* the directed edges in the lock acquisition graph.
*/
private static class LockGraphNode {
/**
* The map tracking the locks that are known to be acquired before this
* lock, each associated with an example stack trace. Locks are weakly keyed
* to allow proper garbage collection when they are no longer referenced.
*/
final Map allowedPriorLocks = new MapMaker().weakKeys().makeMap();
/**
* The map tracking lock nodes that can cause a lock acquisition cycle if
* acquired before this node.
*/
final Map disallowedPriorLocks = new MapMaker().weakKeys().makeMap();
final String lockName;
LockGraphNode(String lockName) {
this.lockName = Preconditions.checkNotNull(lockName);
}
String getLockName() {
return lockName;
}
void checkAcquiredLocks(
Policy policy, List acquiredLocks) {
for (int i = 0, size = acquiredLocks.size(); i < size; i++) {
checkAcquiredLock(policy, acquiredLocks.get(i));
}
}
/**
* Checks the acquisition-ordering between {@code this}, which is about to
* be acquired, and the specified {@code acquiredLock}.
*
* When this method returns, the {@code acquiredLock} should be in either
* the {@code preAcquireLocks} map, for the case in which it is safe to
* acquire {@code this} after the {@code acquiredLock}, or in the
* {@code disallowedPriorLocks} map, in which case it is not safe.
*/
void checkAcquiredLock(Policy policy, LockGraphNode acquiredLock) {
// checkAcquiredLock() should never be invoked by a lock that has already
// been acquired. For unordered locks, aboutToAcquire() ensures this by
// checking isAcquiredByCurrentThread(). For ordered locks, however, this
// can happen because multiple locks may share the same LockGraphNode. In
// this situation, throw an IllegalStateException as defined by contract
// described in the documentation of WithExplicitOrdering.
Preconditions.checkState(this != acquiredLock,
"Attempted to acquire multiple locks with the same rank %s", acquiredLock.getLockName());
if (allowedPriorLocks.containsKey(acquiredLock)) {
// The acquisition ordering from "acquiredLock" to "this" has already
// been verified as safe. In a properly written application, this is
// the common case.
return;
}
PotentialDeadlockException previousDeadlockException = disallowedPriorLocks.get(acquiredLock);
if (previousDeadlockException != null) {
// Previously determined to be an unsafe lock acquisition.
// Create a new PotentialDeadlockException with the same causal chain
// (the example cycle) as that of the cached exception.
PotentialDeadlockException exception = new PotentialDeadlockException(
acquiredLock, this,
previousDeadlockException.getConflictingStackTrace());
policy.handlePotentialDeadlock(exception);
return;
}
// Otherwise, it's the first time seeing this lock relationship. Look for
// a path from the acquiredLock to this.
Set seen = Sets.newIdentityHashSet();
ExampleStackTrace path = acquiredLock.findPathTo(this, seen);
if (path == null) {
// this can be safely acquired after the acquiredLock.
//
// Note that there is a race condition here which can result in missing
// a cyclic edge: it's possible for two threads to simultaneous find
// "safe" edges which together form a cycle. Preventing this race
// condition efficiently without _introducing_ deadlock is probably
// tricky. For now, just accept the race condition---missing a warning
// now and then is still better than having no deadlock detection.
allowedPriorLocks.put(
acquiredLock, new ExampleStackTrace(acquiredLock, this));
} else {
// Unsafe acquisition order detected. Create and cache a
// PotentialDeadlockException.
PotentialDeadlockException exception = new PotentialDeadlockException(acquiredLock, this, path);
disallowedPriorLocks.put(acquiredLock, exception);
policy.handlePotentialDeadlock(exception);
}
}
/**
* Performs a depth-first traversal of the graph edges defined by each
* node's {@code allowedPriorLocks} to find a path between {@code this} and
* the specified {@code lock}.
*
* @return If a path was found, a chained {@link ExampleStackTrace}
* illustrating the path to the {@code lock}, or {@code null} if no path
* was found.
*/
@Nullable
private ExampleStackTrace findPathTo(
LockGraphNode node, Set seen) {
if (!seen.add(this)) {
return null; // Already traversed this node.
}
ExampleStackTrace found = allowedPriorLocks.get(node);
if (found != null) {
return found; // Found a path ending at the node!
}
// Recurse the edges.
for (Map.Entry entry : allowedPriorLocks.entrySet()) {
LockGraphNode preAcquiredLock = entry.getKey();
found = preAcquiredLock.findPathTo(node, seen);
if (found != null) {
// One of this node's allowedPriorLocks found a path. Prepend an
// ExampleStackTrace(preAcquiredLock, this) to the returned chain of
// ExampleStackTraces.
ExampleStackTrace path = new ExampleStackTrace(preAcquiredLock, this);
path.setStackTrace(entry.getValue().getStackTrace());
path.initCause(found);
return path;
}
}
return null;
}
}
/**
* CycleDetectingLock implementations must call this method before attempting
* to acquire the lock.
*/
private void aboutToAcquire(CycleDetectingLock lock) {
if (!lock.isAcquiredByCurrentThread()) {
ArrayList acquiredLockList = acquiredLocks.get();
LockGraphNode node = lock.getLockGraphNode();
node.checkAcquiredLocks(policy, acquiredLockList);
acquiredLockList.add(node);
}
}
/**
* CycleDetectingLock implementations must call this method in a
* {@code finally} clause after any attempt to change the lock state,
* including both lock and unlock attempts. Failure to do so can result in
* corrupting the acquireLocks set.
*/
private void lockStateChanged(CycleDetectingLock lock) {
if (!lock.isAcquiredByCurrentThread()) {
ArrayList acquiredLockList = acquiredLocks.get();
LockGraphNode node = lock.getLockGraphNode();
// Iterate in reverse because locks are usually locked/unlocked in a
// LIFO order.
for (int i = acquiredLockList.size() - 1; i >= 0; i--) {
if (acquiredLockList.get(i) == node) {
acquiredLockList.remove(i);
break;
}
}
}
}
final class CycleDetectingReentrantLock
extends ReentrantLock implements CycleDetectingLock {
private final LockGraphNode lockGraphNode;
private CycleDetectingReentrantLock(
LockGraphNode lockGraphNode, boolean fair) {
super(fair);
this.lockGraphNode = Preconditions.checkNotNull(lockGraphNode);
}
///// CycleDetectingLock methods. /////
@Override
public LockGraphNode getLockGraphNode() {
return lockGraphNode;
}
@Override
public boolean isAcquiredByCurrentThread() {
return isHeldByCurrentThread();
}
///// Overridden ReentrantLock methods. /////
@Override
public void lock() {
aboutToAcquire(this);
try {
super.lock();
} finally {
lockStateChanged(this);
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
aboutToAcquire(this);
try {
super.lockInterruptibly();
} finally {
lockStateChanged(this);
}
}
@Override
public boolean tryLock() {
aboutToAcquire(this);
try {
return super.tryLock();
} finally {
lockStateChanged(this);
}
}
@Override
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
aboutToAcquire(this);
try {
return super.tryLock(timeout, unit);
} finally {
lockStateChanged(this);
}
}
@Override
public void unlock() {
try {
super.unlock();
} finally {
lockStateChanged(this);
}
}
}
final class CycleDetectingReentrantReadWriteLock
extends ReentrantReadWriteLock implements CycleDetectingLock {
// These ReadLock/WriteLock implementations shadow those in the
// ReentrantReadWriteLock superclass. They are simply wrappers around the
// internal Sync object, so this is safe since the shadowed locks are never
// exposed or used.
private final CycleDetectingReentrantReadLock readLock;
private final CycleDetectingReentrantWriteLock writeLock;
private final LockGraphNode lockGraphNode;
private CycleDetectingReentrantReadWriteLock(
LockGraphNode lockGraphNode, boolean fair) {
super(fair);
this.readLock = new CycleDetectingReentrantReadLock(this);
this.writeLock = new CycleDetectingReentrantWriteLock(this);
this.lockGraphNode = Preconditions.checkNotNull(lockGraphNode);
}
///// Overridden ReentrantReadWriteLock methods. /////
@Override
public ReadLock readLock() {
return readLock;
}
@Override
public WriteLock writeLock() {
return writeLock;
}
///// CycleDetectingLock methods. /////
@Override
public LockGraphNode getLockGraphNode() {
return lockGraphNode;
}
@Override
public boolean isAcquiredByCurrentThread() {
return isWriteLockedByCurrentThread() || getReadHoldCount() > 0;
}
}
private class CycleDetectingReentrantReadLock
extends ReentrantReadWriteLock.ReadLock {
@Weak
final CycleDetectingReentrantReadWriteLock readWriteLock;
CycleDetectingReentrantReadLock(
CycleDetectingReentrantReadWriteLock readWriteLock) {
super(readWriteLock);
this.readWriteLock = readWriteLock;
}
@Override
public void lock() {
aboutToAcquire(readWriteLock);
try {
super.lock();
} finally {
lockStateChanged(readWriteLock);
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
aboutToAcquire(readWriteLock);
try {
super.lockInterruptibly();
} finally {
lockStateChanged(readWriteLock);
}
}
@Override
public boolean tryLock() {
aboutToAcquire(readWriteLock);
try {
return super.tryLock();
} finally {
lockStateChanged(readWriteLock);
}
}
@Override
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
aboutToAcquire(readWriteLock);
try {
return super.tryLock(timeout, unit);
} finally {
lockStateChanged(readWriteLock);
}
}
@Override
public void unlock() {
try {
super.unlock();
} finally {
lockStateChanged(readWriteLock);
}
}
}
private class CycleDetectingReentrantWriteLock
extends ReentrantReadWriteLock.WriteLock {
@Weak
final CycleDetectingReentrantReadWriteLock readWriteLock;
CycleDetectingReentrantWriteLock(
CycleDetectingReentrantReadWriteLock readWriteLock) {
super(readWriteLock);
this.readWriteLock = readWriteLock;
}
@Override
public void lock() {
aboutToAcquire(readWriteLock);
try {
super.lock();
} finally {
lockStateChanged(readWriteLock);
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
aboutToAcquire(readWriteLock);
try {
super.lockInterruptibly();
} finally {
lockStateChanged(readWriteLock);
}
}
@Override
public boolean tryLock() {
aboutToAcquire(readWriteLock);
try {
return super.tryLock();
} finally {
lockStateChanged(readWriteLock);
}
}
@Override
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
aboutToAcquire(readWriteLock);
try {
return super.tryLock(timeout, unit);
} finally {
lockStateChanged(readWriteLock);
}
}
@Override
public void unlock() {
try {
super.unlock();
} finally {
lockStateChanged(readWriteLock);
}
}
}
}