11

I'm using a WeakHashMap concurrently. I want to achieve fine-grained locking based on an Integer parameter; if thread A needs to modify a resource identified by Integer a and thread B does the same for resource identified by Integer b, then they need not to be synchronized. However, if there are two threads using the same resource, say thread C is also using a resource identified by Integer a, then of course thread A and C need to synchronize on the same Lock.

When there are no more threads that need the resource with ID X then the Lock in the Map for key=X can be removed. However, another thread can come in at that moment and try to use the lock in the Map for ID=X, so we need global synchronization when adding/removing the lock. (This would be the only place where every thread must synchronize, regardless of the Integer parameter) But, a thread cannot know when to remove the lock, because it doesn't know it is the last thread using the lock.

That's why I'm using a WeakHashMap: when the ID is no longer used, the key-value pair can be removed when the GC wants it.

To make sure I have a strong reference to the key of an already existing entry, and exactly that object reference that forms the key of the mapping, I need to iterate the keySet of the map:

synchronized (mrLocks){
    // ... do other stuff
    for (Integer entryKey : mrLocks.keySet()) {
        if (entryKey.equals(id)) {
            key = entryKey;
            break;
        }
    }
    // if key==null, no thread has a strong reference to the Integer
    // key, so no thread is doing work on resource with id, so we can
    // add a mapping (new Integer(id) => new ReentrantLock()) here as
    // we are in a synchronized block. We must keep a strong reference
    // to the newly created Integer, because otherwise the id-lock mapping
    // may already have been removed by the time we start using it, and 
    // then other threads will not use the same Lock object for this
    // resource
}

Now, can the content of the Map change while iterating it? I think not, because by calling mrLocks.keySet(), I created a strong reference to all keys for the scope of iteration. Is that correct?

Timmos
  • 3,215
  • 2
  • 32
  • 40
  • See: http://stackoverflow.com/questions/2861410/weakhashmap-iteration-and-garbage-collection – ikettu Jan 19 '15 at 11:22
  • 1
    I think not, from [JavaDoc](http://docs.oracle.com/javase/7/docs/api/java/util/WeakHashMap.html#keySet%28%29): *"**The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.**"* – m0skit0 Jan 19 '15 at 11:23
  • @m0skit0 Ah, you may be right. The returned Set will also contain WeakReference's but this is hidden just like WeakHashMap hides it. So I should take a clone of the keySet first, and then iterate the clone, I guess, to make sure I'm iterating a collection with strong references. – Timmos Jan 19 '15 at 11:30
  • Maybe I'm misunderstanding what you are trying to do but I don't really see the importance of the iteration. You either find the entry or you don't. *"But, a thread cannot know when to remove the lock, because it doesn't know it is the last thread using the lock...That's why I'm using a WeakHashMap."* There is a logical jump here which I don't think you explained very well. Is your question about keySet or are you asking for a better design that doesn't use WeakHashMap? http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem – Radiodef Jan 20 '15 at 04:58
  • @Radiodef I wanted the map entries to be removed when no longer necessary. It is impossible to let the last thread do this, so I'm delegating that memory management to the garbage collector by using a WeakHashMap. Well, it would be possible if I used an AtomicInteger, to keep track of the threads using the lock, but then there is so much bookkeeping to do which makes it a mess. The key point is that I wanted to avoid that entries were in the Map forever, because that would be a memory leak. – Timmos Jan 21 '15 at 07:08

1 Answers1

3

As the API makes no assertions about the keySet(), I would recommend a cache usage like this:

private static Map<Integer, Reference<Integer>> lockCache = Collections.synchronizedMap(new WeakHashMap<>());

public static Object getLock(Integer i)
{
    Integer monitor = null;
    synchronized(lockCache) {
        Reference<Integer> old = lockCache.get(i);
        if (old != null)
            monitor = old.get();

        // if no monitor exists yet
        if (monitor == null) {
            /* clone i for avoiding strong references 
               to the map's key besides the Object returend 
               by this method.
            */ 
            monitor = new Integer(i);
            lockCache.remove(monitor); //just to be sure
            lockCache.put(monitor, new WeakReference<>(monitor));
        }

    }

    return monitor;
}

This way you are holding a reference to the monitor (the key itself) while locking on it and allow the GC to finalize it when not using it anymore.

Edit:
After the discussion about payload in the comments I thought about a solution with two caches:

private static Map<Integer, Reference<ReentrantLock>> lockCache = new WeakHashMap<>();
private static Map<ReentrantLock, Integer> keyCache = new WeakHashMap<>();

public static ReentrantLock getLock(Integer i)
{
    ReentrantLock lock = null;
    synchronized(lockCache) {
        Reference<ReentrantLock> old = lockCache.get(i);
        if (old != null)
            lock = old.get();

        // if no lock exists or got cleared from keyCache already but not from lockCache yet
        if (lock == null || !keyCache.containsKey(lock)) {
            /* clone i for avoiding strong references 
               to the map's key besides the Object returend 
               by this method.
           */ 
            Integer cacheKey = new Integer(i); 
            lock = new ReentrantLock();
            lockCache.remove(cacheKey); // just to be sure
            lockCache.put(cacheKey, new WeakReference<>(lock));
            keyCache.put(lock, cacheKey);
        }                
    }

    return lock;
}

As long as a strong reference to the payload (the lock) exists, the strong reference to the mapped integer in keyCache avoids the removal of the payload from the lockCache cache.

flo
  • 9,713
  • 6
  • 25
  • 41
  • Why are you using `Collections.synchronizedMap`? There is already external synchronization using the `synchronized` keyword, so you don't need the internal synchronization? – Timmos Jan 19 '15 at 12:23
  • @Timmos copy&pase :-) You are right, it is basically useless and could be removed. – flo Jan 19 '15 at 12:29
  • 1
    I've inspected your code and this seems clean enough, I already thought about a solution like this. The thing is now that the payload (a real Lock object) needs to be included in the value, which creates the need for a new class that encapsulates both a weakly referenced Integer and a strong referenced Lock object, which was infact what I tried to avoid. Instead of iterating the keys to look for the correct Integer instance, you just keep it stored in the value. Your solution also seems as a valid approach. – Timmos Jan 19 '15 at 12:37
  • @Timmos to avoid another wrapping Object you could just subclass your Payload and add a reference to the key. – flo Jan 19 '15 at 12:39
  • Prefer composition over inheritance :) Also you would be lucky that ReentrantLock is not a final class. Infact it would be the same thing, subclassing is also making a new class. But anyway this is an offtopic discussion. Thanks for your input. – Timmos Jan 19 '15 at 12:42
  • Final question. Don't you think that the check `!lockCache.containsKey(monitor)` is obsolete? It is only evaluated in case that `monitor != null`, which means you succeeded in having a strong reference to the Integer. This means that the entry cannot have been cleared from the Map, and so `!lockCache.containsKey(monitor)` will always evaluate to `false`, rendering it useless in the or-operator. – Timmos Jan 19 '15 at 13:16
  • You are right. Edited it to `!keyCache.contains(lock)` to avoid situations where the `keyCache` is cleared from an obsolete lock while the `lockCache` is not yet and thus ending up in a situation where a lock is gotten from the cache which has no more existing mapping to the key. – flo Jan 19 '15 at 13:26
  • I'm sorry to elaborate so much on this, but I think in your first example, you can just delete the check. – Timmos Jan 19 '15 at 13:39
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/69130/discussion-between-timmos-and-flo). – Timmos Jan 19 '15 at 14:10