I arrive at a somewhat different solution when taking into account the "random" refresh triggered by a user. Also, I think the first fetch should wait for the cache to be filled (i.e. wait for first cached object to be created). And, finally, there should be some (unit) tests to verify the cache works as intended and is thread-safe.
First the cache implementation:
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
// http://stackoverflow.com/q/31338509/3080094
public enum DbCachedObject {
INSTANCE;
private final CountDownLatch initLock = new CountDownLatch(1);
private final Object refreshLock = new Object();
private final AtomicReference<CachedObject> cachedInstance = new AtomicReference<CachedObject>();
private final AtomicLong lastUpdate = new AtomicLong();
private volatile boolean refreshing;
private long cachePeriodMs = 1000L; // make this an AtomicLong if it can be updated
public CachedObject get() {
CachedObject o = cachedInstance.get();
if (o == null || isCacheOutdated()) {
updateCache();
if (o == null) {
awaitInit();
o = cachedInstance.get();
}
}
return o;
}
public void refresh() {
updateCache();
}
private boolean isCacheOutdated() {
return (System.currentTimeMillis() - lastUpdate.get() > cachePeriodMs);
}
private void updateCache() {
synchronized (refreshLock) {
// prevent users from refreshing while an update is already in progress
if (refreshing) {
return;
}
refreshing = true;
// prevent other threads from calling this method again
lastUpdate.set(System.currentTimeMillis());
}
new Thread() {
@Override
public void run() {
try {
cachedInstance.set(getFromDb());
// set the 'real' last update time
lastUpdate.set(System.currentTimeMillis());
initLock.countDown();
} finally {
// make sure refreshing is set to false, even in case of error
refreshing = false;
}
}
}.start();
}
private boolean awaitInit() {
boolean initialized = false;
try {
// assume cache-period is longer as the time it takes to create the cached object
initialized = initLock.await(cachePeriodMs, TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
return initialized;
}
private CachedObject getFromDb() {
// dummy call, no db is involved
return new CachedObject();
}
public long getCachePeriodMs() {
return cachePeriodMs;
}
}
Second the cached object with a main-method that tests the cache implementation:
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class CachedObject {
private static final AtomicInteger createCount = new AtomicInteger();
static final long createTimeMs = 100L;
private final int instanceNumber = createCount.incrementAndGet();
public CachedObject() {
println("Creating cached object " + instanceNumber);
try {
Thread.sleep(createTimeMs);
} catch (Exception ignored) {}
println("Cached object " + instanceNumber + " created");
}
public int getInstanceNumber() {
return instanceNumber;
}
@Override
public String toString() {
return getClass().getSimpleName() + "-" + getInstanceNumber();
}
private static final long startTime = System.currentTimeMillis();
/**
* Test the use of DbCachedObject.
*/
public static void main(String[] args) {
ThreadPoolExecutor tp = (ThreadPoolExecutor) Executors.newCachedThreadPool();
final int tcount = 2; // amount of tasks running in paralllel
final long threadStartGracePeriodMs = 50L; // starting runnables takes time
try {
// verify first calls wait for initialization of first cached object
fetchCacheTasks(tp, tcount, createTimeMs + threadStartGracePeriodMs);
// verify immediate return of cached object
CachedObject o = DbCachedObject.INSTANCE.get();
println("Cached: " + o);
// wait for refresh-period
Thread.sleep(DbCachedObject.INSTANCE.getCachePeriodMs() + 1);
// trigger update
o = DbCachedObject.INSTANCE.get();
println("Triggered update for " + o);
// wait for update to complete
Thread.sleep(createTimeMs + 1);
// verify updated cached object is returned
fetchCacheTasks(tp, tcount, threadStartGracePeriodMs);
// trigger update
DbCachedObject.INSTANCE.refresh();
// wait for update to complete
Thread.sleep(createTimeMs + 1);
println("Refreshed: " + DbCachedObject.INSTANCE.get());
} catch (Exception e) {
e.printStackTrace();
} finally {
tp.shutdownNow();
}
}
private static void fetchCacheTasks(ThreadPoolExecutor tp, int tasks, long doneWaitTimeMs) throws Exception {
final CountDownLatch fetchStart = new CountDownLatch(tasks);
final CountDownLatch fetchDone = new CountDownLatch(tasks);
// println("Starting " + tasks + " tasks");
for (int i = 0; i < tasks; i++) {
final int r = i;
tp.execute(new Runnable() {
@Override public void run() {
fetchStart.countDown();
try { fetchStart.await();} catch (Exception ignored) {}
CachedObject o = DbCachedObject.INSTANCE.get();
println("Task " + r + " got " + o);
fetchDone.countDown();
}
});
}
println("Awaiting " + tasks + " tasks");
if (!fetchDone.await(doneWaitTimeMs, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Fetch cached object tasks incomplete.");
}
}
private static void println(String msg) {
System.out.println((System.currentTimeMillis() - startTime) + " " + msg);
}
}
The tests in the main-method need human eyes to verify the results, but they should provide sufficient input for unit tests. Once the unit tests are more refined, the cache implementation will probably need some finishing touches as well.
bare with meunless you're going to a nudist resort..