The requirement seems to be this:
- There is a
Map<String, Object>
that is a cache.
- There are a number of worker threads in a pool the access the cache
- Some types of work require the object in the cache to be invalidated when they are done
First you will need a ConcurrentHashMap<String, Lock> keys
. This Map
will store a relationship between the String
keys and and Lock
objects that we will use the lock the keys. This allows us to replace the key -> value
mappings without locking the entire data Map
.
Next you will need a ConcurrentHashMap<String, Object> data
. This Map
will store the actual mappings.
The reason to use a ConcurrentHashMap
rather than a plain one is that it is thread safe. This means that manually synchronizing is not required. The implementation actually divides the Map
into sectors and only locks the required sector to carry out operations - this makes it more efficient.
Now, the logic will be
putIfAbsent
a new ReentrantLock
into keys
. This will, in a thread safe manner, check if a lock is already present for a key
. If not a new one will be added, otherwise the existing one is retrieved. This means that there will only ever be one lock per key
- Acquire a lock. This means that you gain exclusive access to a mapping.
- Do work. In the case of
TypeII
remove the mapping from data
after finishing.
- Unlock the lock.
The code would look something like this:
private final ConcurrentHashMap<String, Object> data = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Lock> keys = new ConcurrentHashMap<>();
private final ExecutorService executorService = null; //obviously make one of these
@RequiredArgsConstructor
private class TypeI implements Runnable {
private final String key;
private final Work work;
@Override
public void run() {
final Lock lock = keys.putIfAbsent(key, new ReentrantLock());
lock.lock();
try {
final Object value = data.get(key);
work.doWork(value);
} finally {
lock.unlock();
}
}
}
@RequiredArgsConstructor
private class TypeII implements Runnable {
private final String key;
private final Work work;
@Override
public void run() {
final Lock lock = keys.putIfAbsent(key, new ReentrantLock());
lock.lock();
try {
final Object value = data.get(key);
work.doWork(value);
data.remove(key);
} finally {
lock.unlock();
}
}
}
public static interface Work {
void doWork(Object value);
}
public void doTypeIWork(final String key, final Work work) {
executorService.submit(new TypeI(key, work));
}
public void doTypeIIWork(final String key, final Work work) {
executorService.submit(new TypeII(key, work));
}
I have used Lombok
annotations to reduce the amount of clutter.
The idea is to minimise, or almost eliminate, the amount of common resource locking while still allowing a Thread
to gain, if needed, exclusive access to a particular mapping.
To clean the keys Map
you would need to guarantee that no work is currently ongoing and that no Thread
s would try and acquire any locks during the cleaning period. You could do this by attempting to acquire the relevant lock and then removing the mapping from the keys map - this would ensure no other thread was using the lock at the time.
You could run a scheduled task that clears, say, 20 keys from the map every X minutes. If you implemented it as an LRU cache then it should be fairly clean. Google Guava provide an implementation that you could use.