In my application I need to ensure that for an entity representing a data row in a database I have at most one java object representing it.
Ensuring that they are equals() is not enough, since I could get caught by coherency problems. So basically I need a multiton; moreover, I need not to keep this object in memory when it is not necessary, so I will be using weak references.
I have devised this solution:
package com.example;
public class DbEntity {
// a DbEntity holds a strong reference to its key, so as long as someone holds a
// reference to it the key won't be evicted from the WeakHashMap
private String key;
public void setKey(String key) {
this.key = key;
}
public String getKey() {
return key;
}
//other stuff that makes this object actually useful.
}
package com.example;
import java.lang.ref.WeakReference;
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReentrantLock;
public class WeakMultiton {
private ReentrantLock mapLock = new ReentrantLock();
private WeakHashMap<String, WeakReference<DbEntity>> entityMap = new WeakHashMap<String, WeakReference<DbEntity>>();
private void fill(String key, DbEntity object) throws Exception {
// do slow stuff, typically fetch data from DB and fill the object.
}
public DbEntity get(String key) throws Exception {
DbEntity result = null;
WeakReference<DbEntity> resultRef = entityMap.get(key);
if (resultRef != null){
result = resultRef.get();
}
if (result == null){
mapLock.lock();
try {
resultRef = entityMap.get(key);
if (resultRef != null){
result = resultRef.get();
}
if (result == null){
result = new DbEntity();
synchronized (result) {
// A DbEntity holds a strong reference to its key, so the key won't be evicted from the map
// as long as result is reachable.
entityMap.put(key, new WeakReference<DbEntity>(result));
// I unlock the map, but result is still locked.
// Keeping the map locked while querying the DB would serialize database calls!
// If someone tries to get the same DbEntity the method will wait to return until I get out of this synchronized block.
mapLock.unlock();
fill(key, result);
// I need the key to be exactly this String, not just an equal one!!
result.setKey(key);
}
}
} finally {
// I have to check since I could have already released the lock.
if (mapLock.isHeldByCurrentThread()){
mapLock.unlock();
}
}
}
// I synchronize on result since some other thread could have instantiated it but still being busy initializing it.
// A performance penality, but still better than synchronizing on the whole map.
synchronized (result) {
return result;
}
}
}
WeakMultiton will be instantiated only in the database wrapper (single point of access to the database) and its get(String key) will of course be the only way to retrieve a DbEntity. Now, to the best of my knowledge this should work, but since this stuff is pretty new to me, I fear I could be overseeing something about the synchronization or the weak references! Can you spot any flaw or suggest improvements?
I found out about guava's MapMaker and wrote this generic AbstractWeakMultiton:
It has the following advantages to my earlier try:
It does not depend on the fact that values hold a strong reference to the key It does not need to do the awkward double checking for expired weak references It is reusable
On the other hand, it depends on the rather beefy Guava library, while the first solution used just classes from the runtime environment. I can live with that.
I'm obviously still looking for further improvements and error spotting, and basically everything that answers the most important question: will it work?