Home | History | Annotate | Download | only in internal
      1 package com.google.inject.internal;
      2 
      3 import com.google.common.base.Preconditions;
      4 import com.google.common.base.Supplier;
      5 import com.google.common.collect.ArrayListMultimap;
      6 import com.google.common.collect.ImmutableListMultimap;
      7 import com.google.common.collect.LinkedHashMultimap;
      8 import com.google.common.collect.ListMultimap;
      9 import com.google.common.collect.Lists;
     10 import com.google.common.collect.Maps;
     11 import com.google.common.collect.Multimap;
     12 import com.google.common.collect.Multimaps;
     13 
     14 import java.util.Collection;
     15 import java.util.LinkedHashMap;
     16 import java.util.List;
     17 import java.util.Map;
     18 import java.util.concurrent.locks.Lock;
     19 import java.util.concurrent.locks.ReentrantLock;
     20 
     21 /**
     22  * Simplified version of {@link Lock} that is special due to how it handles deadlocks detection.
     23  *
     24  * <p>Is an inherent part of {@link SingletonScope}, moved into a upper level class due
     25  * to its size and complexity.
     26  *
     27  * @param  Lock identification provided by the client, is returned unmodified to the client
     28  *        when lock cycle is detected to identify it. Only toString() needs to be implemented.
     29  *        Lock references this object internally,
     30  *        for the purposes of Garbage Collection you should not use heavy IDs.
     31  *        Lock is referenced by a lock factory as long as it's owned by a thread.
     32  *
     33  * @see SingletonScope
     34  * @see com.google.inject.internal.CycleDetectingLock.CycleDetectingLockFactory
     35  *
     36  * @author timofeyb (Timothy Basanov)
     37  */
     38 interface CycleDetectingLock<ID> {
     39 
     40   /**
     41    * Takes a lock in a blocking fashion in case no potential deadlocks are detected.
     42    * If the lock was successfully owned, returns an empty map indicating no detected potential
     43    * deadlocks.
     44    *
     45    * Otherwise, a map indicating threads involved in a potential deadlock are returned.
     46    * Map is ordered by dependency cycle and lists locks for each thread that are part of
     47    * the loop in order. Returned map is created atomically.
     48    *
     49    * In case no cycle is detected performance is O(threads creating singletons),
     50    * in case cycle is detected performance is O(singleton locks).
     51    */
     52   ListMultimap<Long, ID> lockOrDetectPotentialLocksCycle();
     53 
     54   /**
     55    * Unlocks previously locked lock.
     56    */
     57   void unlock();
     58 
     59   /**
     60    * Wraps locks so they would never cause a deadlock. On each
     61    * {@link CycleDetectingLock#lockOrDetectPotentialLocksCycle} we check for dependency cycles
     62    * within locks created by the same factory. Either we detect a cycle and return it
     63    * or take it atomically.
     64    *
     65    * <p>Important to note that we do not prevent deadlocks in the client code. As an example:
     66    * Thread A takes lock L and creates singleton class CA depending on the singleton class CB.
     67    * Meanwhile thread B is creating class CB and is waiting on the lock L. Issue happens
     68    * due to client code creating interdependent classes and using locks, where
     69    * no guarantees on the creation order from Guice are provided.
     70    *
     71    * <p>Instances of these locks are not intended to be exposed outside of {@link SingletonScope}.
     72    */
     73   class CycleDetectingLockFactory<ID> {
     74 
     75     /**
     76      * Specifies lock that thread is currently waiting on to own it.
     77      * Used only for purposes of locks cycle detection.
     78      *
     79      * Key: thread id
     80      * Value: lock that is being waited on
     81      *
     82      * Element is added inside {@link #lockOrDetectPotentialLocksCycle()} before {@link Lock#lock}
     83      * is called. Element is removed inside {@link #lockOrDetectPotentialLocksCycle()} after
     84      * {@link Lock#lock} and synchronously with adding it to {@link #locksOwnedByThread}.
     85      *
     86      * Same lock can be added for several threads in case all of them are trying to
     87      * take it.
     88      *
     89      * Guarded by {@code this}.
     90      */
     91     private Map<Long, ReentrantCycleDetectingLock> lockThreadIsWaitingOn = Maps.newHashMap();
     92 
     93     /**
     94      * Lists locks that thread owns.
     95      * Used only to populate locks in a potential cycle when it is detected.
     96      *
     97      * Key: thread id
     98      * Value: stack of locks that were owned.
     99      *
    100      * Element is added inside {@link #lockOrDetectPotentialLocksCycle()} after {@link Lock#lock}
    101      * is called. Element is removed inside {@link #unlock()} synchronously with
    102      * {@link Lock#unlock()} call.
    103      *
    104      * Same lock can only be present several times for the same thread as locks are
    105      * reentrant. Lock can not be owned by several different threads as the same time.
    106      *
    107      * Guarded by {@code this}.
    108      */
    109     private final Multimap<Long, ReentrantCycleDetectingLock> locksOwnedByThread =
    110         LinkedHashMultimap.create();
    111 
    112     /**
    113      * Creates new lock within this factory context. We can guarantee that locks created by
    114      * the same factory would not deadlock.
    115      *
    116      * @param newLockId lock id that would be used to report lock cycles if detected
    117      */
    118     CycleDetectingLock<ID> create(ID newLockId) {
    119       return new ReentrantCycleDetectingLock(newLockId, new ReentrantLock());
    120     }
    121 
    122     /** The implementation for {@link CycleDetectingLock}. */
    123     class ReentrantCycleDetectingLock implements CycleDetectingLock<ID> {
    124 
    125       /** Underlying lock used for actual waiting when no potential deadlocks are detected. */
    126       private final Lock lockImplementation;
    127       /** User id for this lock. */
    128       private final ID userLockId;
    129       /**
    130        * Thread id for the thread that owned this lock. Nullable.
    131        * Guarded by {@code CycleDetectingLockFactory.this}.
    132        */
    133       private Long lockOwnerThreadId = null;
    134       /**
    135        * Number of times that thread owned this lock.
    136        * Guarded by {@code CycleDetectingLockFactory.this}.
    137        */
    138       private int lockReentranceCount = 0;
    139 
    140       ReentrantCycleDetectingLock(ID userLockId, Lock lockImplementation) {
    141         this.userLockId = Preconditions.checkNotNull(userLockId, "userLockId");
    142         this.lockImplementation = Preconditions.checkNotNull(
    143             lockImplementation, "lockImplementation");
    144       }
    145 
    146       @Override public ListMultimap<Long, ID> lockOrDetectPotentialLocksCycle() {
    147         final long currentThreadId = Thread.currentThread().getId();
    148         synchronized (CycleDetectingLockFactory.this) {
    149           checkState();
    150           ListMultimap<Long, ID> locksInCycle = detectPotentialLocksCycle();
    151           if (!locksInCycle.isEmpty()) {
    152             // potential deadlock is found, we don't try to take this lock
    153             return locksInCycle;
    154           }
    155 
    156           lockThreadIsWaitingOn.put(currentThreadId, this);
    157         }
    158 
    159         // this may be blocking, but we don't expect it to cause a deadlock
    160         lockImplementation.lock();
    161 
    162         synchronized (CycleDetectingLockFactory.this) {
    163           // current thread is no longer waiting on this lock
    164           lockThreadIsWaitingOn.remove(currentThreadId);
    165           checkState();
    166 
    167           // mark it as owned by us
    168           lockOwnerThreadId = currentThreadId;
    169           lockReentranceCount++;
    170           // add this lock to the list of locks owned by a current thread
    171           locksOwnedByThread.put(currentThreadId, this);
    172         }
    173         // no deadlock is found, locking successful
    174         return ImmutableListMultimap.of();
    175       }
    176 
    177       @Override public void unlock() {
    178         final long currentThreadId = Thread.currentThread().getId();
    179         synchronized (CycleDetectingLockFactory.this) {
    180           checkState();
    181           Preconditions.checkState(lockOwnerThreadId != null,
    182               "Thread is trying to unlock a lock that is not locked");
    183           Preconditions.checkState(lockOwnerThreadId == currentThreadId,
    184               "Thread is trying to unlock a lock owned by another thread");
    185 
    186           // releasing underlying lock
    187           lockImplementation.unlock();
    188 
    189           // be sure to release the lock synchronously with updating internal state
    190           lockReentranceCount--;
    191           if (lockReentranceCount == 0) {
    192             // we no longer own this lock
    193             lockOwnerThreadId = null;
    194             Preconditions.checkState(locksOwnedByThread.remove(currentThreadId, this),
    195                 "Internal error: Can not find this lock in locks owned by a current thread");
    196             if (locksOwnedByThread.get(currentThreadId).isEmpty()) {
    197               // clearing memory
    198               locksOwnedByThread.removeAll(currentThreadId);
    199             }
    200           }
    201         }
    202       }
    203 
    204       /** Check consistency of an internal state. */
    205       void checkState() throws IllegalStateException {
    206         final long currentThreadId = Thread.currentThread().getId();
    207         Preconditions.checkState(!lockThreadIsWaitingOn.containsKey(currentThreadId),
    208             "Internal error: Thread should not be in a waiting thread on a lock now");
    209         if (lockOwnerThreadId != null) {
    210           // check state of a locked lock
    211           Preconditions.checkState(lockReentranceCount >= 0,
    212               "Internal error: Lock ownership and reentrance count internal states do not match");
    213           Preconditions.checkState(locksOwnedByThread.get(lockOwnerThreadId).contains(this),
    214               "Internal error: Set of locks owned by a current thread and lock "
    215                   + "ownership status do not match");
    216         } else {
    217           // check state of a non locked lock
    218           Preconditions.checkState(lockReentranceCount == 0,
    219               "Internal error: Reentrance count of a non locked lock is expect to be zero");
    220           Preconditions.checkState(!locksOwnedByThread.values().contains(this),
    221               "Internal error: Non locked lock should not be owned by any thread");
    222         }
    223       }
    224 
    225       /**
    226        * Algorithm to detect a potential lock cycle.
    227        *
    228        * For lock's thread owner check which lock is it trying to take.
    229        * Repeat recursively. When current thread is found a potential cycle is detected.
    230        *
    231        * @see CycleDetectingLock#lockOrDetectPotentialLocksCycle()
    232        */
    233       private ListMultimap<Long, ID> detectPotentialLocksCycle() {
    234         final long currentThreadId = Thread.currentThread().getId();
    235         if (lockOwnerThreadId == null || lockOwnerThreadId == currentThreadId) {
    236           // if nobody owns this lock, lock cycle is impossible
    237           // if a current thread owns this lock, we let Guice to handle it
    238           return ImmutableListMultimap.of();
    239         }
    240 
    241         ListMultimap<Long, ID> potentialLocksCycle = Multimaps.newListMultimap(
    242             new LinkedHashMap<Long, Collection<ID>>(),
    243             new Supplier<List<ID>>() {
    244               @Override
    245               public List<ID> get() {
    246                 return Lists.newArrayList();
    247               }
    248             });
    249         // lock that is a part of a potential locks cycle, starts with current lock
    250         ReentrantCycleDetectingLock lockOwnerWaitingOn = this;
    251         // try to find a dependency path between lock's owner thread and a current thread
    252         while (lockOwnerWaitingOn != null && lockOwnerWaitingOn.lockOwnerThreadId != null) {
    253           Long threadOwnerThreadWaits = lockOwnerWaitingOn.lockOwnerThreadId;
    254           // in case locks cycle exists lock we're waiting for is part of it
    255           potentialLocksCycle.putAll(threadOwnerThreadWaits,
    256               getAllLockIdsAfter(threadOwnerThreadWaits, lockOwnerWaitingOn));
    257 
    258           if (threadOwnerThreadWaits == currentThreadId) {
    259             // owner thread depends on current thread, cycle detected
    260             return potentialLocksCycle;
    261           }
    262           // going for the next thread we wait on indirectly
    263           lockOwnerWaitingOn = lockThreadIsWaitingOn.get(threadOwnerThreadWaits);
    264         }
    265         // no dependency path from an owner thread to a current thread
    266         return ImmutableListMultimap.of();
    267       }
    268 
    269       /** Return locks owned by a thread after a lock specified, inclusive. */
    270       private List<ID> getAllLockIdsAfter(long threadId, ReentrantCycleDetectingLock lock) {
    271         List<ID> ids = Lists.newArrayList();
    272         boolean found = false;
    273         Collection<ReentrantCycleDetectingLock> ownedLocks = locksOwnedByThread.get(threadId);
    274         Preconditions.checkNotNull(ownedLocks,
    275             "Internal error: No locks were found taken by a thread");
    276         for (ReentrantCycleDetectingLock ownedLock : ownedLocks) {
    277           if (ownedLock == lock) {
    278             found = true;
    279           }
    280           if (found) {
    281             ids.add(ownedLock.userLockId);
    282           }
    283         }
    284         Preconditions.checkState(found, "Internal error: We can not find locks that "
    285             + "created a cycle that we detected");
    286         return ids;
    287       }
    288 
    289       @Override public String toString() {
    290         // copy is made to prevent a data race
    291         // no synchronization is used, potentially stale data, should be good enough
    292         Long localLockOwnerThreadId = this.lockOwnerThreadId;
    293         if (localLockOwnerThreadId != null) {
    294           return String.format("CycleDetectingLock[%s][locked by %s]",
    295               userLockId, localLockOwnerThreadId);
    296         } else {
    297           return String.format("CycleDetectingLock[%s][unlocked]", userLockId);
    298         }
    299       }
    300     }
    301   }
    302 }
    303