summaryrefslogtreecommitdiffstats
path: root/guava/src/com/google/common/util/concurrent/CycleDetectingLockFactory.java
diff options
context:
space:
mode:
Diffstat (limited to 'guava/src/com/google/common/util/concurrent/CycleDetectingLockFactory.java')
-rw-r--r--guava/src/com/google/common/util/concurrent/CycleDetectingLockFactory.java1034
1 files changed, 1034 insertions, 0 deletions
diff --git a/guava/src/com/google/common/util/concurrent/CycleDetectingLockFactory.java b/guava/src/com/google/common/util/concurrent/CycleDetectingLockFactory.java
new file mode 100644
index 0000000..53d19d0
--- /dev/null
+++ b/guava/src/com/google/common/util/concurrent/CycleDetectingLockFactory.java
@@ -0,0 +1,1034 @@
+/*
+ * Copyright (C) 2011 The Guava Authors
+ *
+ * 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.google.common.util.concurrent;
+
+import com.google.common.annotations.Beta;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.MapMaker;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+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.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;
+
+/**
+ * The {@code CycleDetectingLockFactory} creates {@link ReentrantLock}s and
+ * {@link ReentrantReadWriteLock}s that detect potential deadlock by checking
+ * for cycles in lock acquisition order.
+ * <p>
+ * 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:
+ * <ul>
+ * <li>DISABLED
+ * <li>WARN
+ * <li>THROW
+ * </ul>
+ * 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.
+ * <p>
+ * 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 <strong>Performance</strong>).
+ * <p>
+ * <strong>Cycle Detection</strong>
+ * <p>
+ * 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:
+ * <pre>
+ * Thread1: acquire(LockA) --X acquire(LockB)
+ * Thread2: acquire(LockB) --X acquire(LockA)
+ * </pre>
+ * 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:
+ * <pre>
+ * Thread1: acquire(LockA) --X acquire(LockB)
+ * Thread2: acquire(LockB) --X acquire(LockC)
+ * ...
+ * ThreadN: acquire(LockN) --X acquire(LockA)
+ * </pre>
+ * 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.
+ * <ul>
+ * <li>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).
+ * <li>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.
+ * <li>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.
+ * <li>If a cycle is detected, an "unsafe" (cyclic) edge is created to represent
+ * a potential deadlock situation, and the appropriate Policy is executed.
+ * </ul>
+ * 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:
+ * <pre>
+ * LockA -&gt; LockB -&gt; LockC
+ * LockA -&gt; LockC -&gt; LockB
+ * </pre>
+ *
+ * <strong>ReadWriteLocks</strong>
+ * <p>
+ * While {@code ReadWriteLock}s have different properties and can form cycles
+ * without potential deadlock, this class treats {@code ReadWriteLock}s 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.
+ * <p>
+ * <strong>Explicit Lock Acquisition Ordering</strong>
+ * <p>
+ * The {@link CycleDetectingLockFactory.WithExplicitOrdering} class can be used
+ * to enforce an application-specific ordering in addition to performing general
+ * cycle detection.
+ * <p>
+ * <strong>Garbage Collection</strong>
+ * <p>
+ * In order to allow proper garbage collection of unused locks, the edges of
+ * the lock graph are weak references.
+ * <p>
+ * <strong>Performance</strong>
+ * <p>
+ * The extra bookkeeping done by cycle detecting locks comes at some cost to
+ * performance. Benchmarks (as of December 2011) show that:
+ *
+ * <ul>
+ * <li>for an unnested {@code lock()} and {@code unlock()}, a cycle detecting
+ * lock takes 38ns as opposed to the 24ns taken by a plain lock.
+ * <li>for nested locking, the cost increases with the depth of the nesting:
+ * <ul>
+ * <li> 2 levels: average of 64ns per lock()/unlock()
+ * <li> 3 levels: average of 77ns per lock()/unlock()
+ * <li> 4 levels: average of 99ns per lock()/unlock()
+ * <li> 5 levels: average of 103ns per lock()/unlock()
+ * <li>10 levels: average of 184ns per lock()/unlock()
+ * <li>20 levels: average of 393ns per lock()/unlock()
+ * </ul>
+ * </ul>
+ *
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * Note that locks created by a factory with this policy will <em>not</em>
+ * 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 Map<Class<? extends Enum>,
+ Map<? extends Enum, LockGraphNode>> lockGraphNodesPerType =
+ new MapMaker().weakKeys().makeComputingMap(
+ new OrderedLockGraphNodesCreator());
+
+ /**
+ * Creates a {@code CycleDetectingLockFactory.WithExplicitOrdering<E>}.
+ */
+ public static <E extends Enum<E>> WithExplicitOrdering<E>
+ newInstanceWithExplicitOrdering(Class<E> enumClass, Policy policy) {
+ // OrderedLockGraphNodesCreator maps each enumClass to a Map with the
+ // corresponding enum key type.
+ @SuppressWarnings("unchecked")
+ Map<E, LockGraphNode> lockGraphNodes =
+ (Map<E, LockGraphNode>) lockGraphNodesPerType.get(enumClass);
+ return new WithExplicitOrdering<E>(policy, lockGraphNodes);
+ }
+
+ /**
+ * 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:
+ *
+ * <pre> {@code
+ * enum MyLockOrder {
+ * FIRST, SECOND, THIRD;
+ * }
+ *
+ * CycleDetectingLockFactory.WithExplicitOrdering<MyLockOrder> 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
+ * }</pre>
+ *
+ * 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.
+ * <p>
+ * 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:
+ *
+ * <pre> {@code
+ * CycleDetectingLockFactory.WithExplicitOrdering<MyLockOrder> factory1 =
+ * CycleDetectingLockFactory.newInstanceWithExplicitOrdering(...);
+ * CycleDetectingLockFactory.WithExplicitOrdering<MyLockOrder> 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
+ * }</pre>
+ *
+ * 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 <E> The Enum type representing the explicit lock ordering.
+ * @since 13.0
+ */
+ @Beta
+ public static final class WithExplicitOrdering<E extends Enum<E>>
+ extends CycleDetectingLockFactory {
+
+ private final Map<E, LockGraphNode> lockGraphNodes;
+
+ @VisibleForTesting
+ WithExplicitOrdering(
+ Policy policy, Map<E, LockGraphNode> 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);
+ }
+ }
+
+ /**
+ * 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 class OrderedLockGraphNodesCreator
+ implements Function<Class<? extends Enum>,
+ Map<? extends Enum, LockGraphNode>> {
+
+ @Override
+ @SuppressWarnings("unchecked") // There's no way to properly express with
+ // wildcards the recursive Enum type required by createNodesFor(), and the
+ // Map/Function types must use wildcards since they accept any Enum class.
+ public Map<? extends Enum, LockGraphNode> apply(
+ Class<? extends Enum> clazz) {
+ return createNodesFor(clazz);
+ }
+
+ <E extends Enum<E>> Map<E, LockGraphNode> createNodesFor(Class<E> clazz) {
+ EnumMap<E, LockGraphNode> map = Maps.newEnumMap(clazz);
+ E[] keys = clazz.getEnumConstants();
+ final int numKeys = keys.length;
+ ArrayList<LockGraphNode> 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 String getLockName(Enum<?> rank) {
+ return rank.getDeclaringClass().getSimpleName() + "." + rank.name();
+ }
+ }
+
+ //////// Implementation /////////
+
+ private static final Logger logger = Logger.getLogger(
+ CycleDetectingLockFactory.class.getName());
+
+ final Policy policy;
+
+ private CycleDetectingLockFactory(Policy policy) {
+ this.policy = 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<ArrayList<LockGraphNode>>
+ acquiredLocks = new ThreadLocal<ArrayList<LockGraphNode>>() {
+ @Override
+ protected ArrayList<LockGraphNode> initialValue() {
+ return Lists.<LockGraphNode>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.
+ *
+ * <pre>
+ * com...ExampleStackTrace: LockB -&gt; LockC
+ * at com...CycleDetectingReentrantLock.lock(CycleDetectingLockFactory.java:443)
+ * at ...
+ * at ...
+ * at com...MyClass.someMethodThatAcquiresLockB(MyClass.java:123)
+ * </pre>
+ */
+ private static class ExampleStackTrace extends IllegalStateException {
+
+ static final StackTraceElement[] EMPTY_STACK_TRACE =
+ new StackTraceElement[0];
+
+ static Set<String> 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}s to illustrate the
+ * cycle, e.g.
+ *
+ * <pre>
+ * com....PotentialDeadlockException: Potential Deadlock from LockC -&gt; ReadWriteA
+ * at ...
+ * at ...
+ * Caused by: com...ExampleStackTrace: LockB -&gt; LockC
+ * at ...
+ * at ...
+ * Caused by: com...ExampleStackTrace: ReadWriteA -&gt; LockB
+ * at ...
+ * at ...
+ * </pre>
+ *
+ * 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<LockGraphNode, ExampleStackTrace> allowedPriorLocks =
+ new MapMaker().weakKeys().makeMap();
+
+ /**
+ * The map tracking lock nodes that can cause a lock acquisition cycle if
+ * acquired before this node.
+ */
+ final Map<LockGraphNode, PotentialDeadlockException>
+ 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<LockGraphNode> 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}.
+ * <p>
+ * 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 " +
+ 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<LockGraphNode> 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<LockGraphNode> 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<LockGraphNode, ExampleStackTrace> 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<LockGraphNode> 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<LockGraphNode> 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 {
+
+ 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 {
+
+ 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);
+ }
+ }
+ }
+}