How to do a lazy create and set with AtomicReference in a safe and efficient manner?
That is not a great system. The problem is that two threads may find that the result == null
, and both will set the fCachedValue
to their new result value.
You want to use the compareAndSet(...) method:
AtomicReference<V> fCachedValue = new AtomicReference<>();
public V getLazy() {
V result = fCachedValue.get();
if (result == null) {
result = costlyIdempotentOperation();
if (!fCachedValue.compareAndSet(null, result)) {
return fCachedValue.get();
}
}
return result;
}
If multiple threads get in to the method before it has been initialized, they may all try to create the large result instance. They will all create their own version of it, but the first one to complete the process will be the one who gets to store their result in the AtomicReference. The other threads will complete their work, then dispose of their result
and instead use the result
instance created by the 'winner'.
For a similar purpose I implemented OnceEnteredCallable which returns a ListenableFuture
for a result. The advantage is that the other threads are not being blocked and this costly operation is being called once.
Usage (requires Guava):
Callable<V> costlyIdempotentOperation = new Callable<>() {...};
// this would block only the thread to execute the callable
ListenableFuture<V> future = new OnceEnteredCallable<>().runOnce(costlyIdempotentOperation);
// this would block all the threads and set the reference
fCachedValue.set(future.get());
// this would set the reference upon computation, Java 8 syntax
future.addListener(() -> {fCachedValue.set(future.get())}, executorService);
This expands the answer by @TwoThe on how AtomicReference<Future<V>>
may be used.
Basically, if you don't mind having (a little bit more expensive) synchronized
sections in your code, the easiest (and the most readable) solution would be to use the Double-checked Locking idiom (with volatile
).
If you still want to utilize the CAS (this is what the whole family of Atomic*
types is about), you have to use AtomicReference<Future<V>>
, not AtomicReference<V>
(or you may end up having multiple threads computing the same expensive value).
But here's another catch: you may obtain a valid Future<V>
instance and share it between multiple threads, but the instance itself may be unusable because your costly computation may have failed. This leads us to the need to re-set the atomic reference we have (fCachedValue.set(null)
) in some or all exceptional situations.
The above implies that it's no longer sufficient to call fCachedValue.compareAndSet(null, new FutureTask(...))
once -- you'll have to atomically test whether the reference contains a non-null
value and re-initialize it if necessary (on each invocation). Luckily, the AtomicReference
class has the getAndUpdate(...)
method which merely invokes compareAndSet(...)
in a loop. So the resulting code might look like this:
class ConcurrentLazy<V> implements Callable<V> {
private final AtomicReference<Future<V>> fCachedValue = new AtomicReference<>();
private final Callable<V> callable;
public ConcurrentLazy(final Callable<V> callable) {
this.callable = callable;
}
/**
* {@inheritDoc}
*
* @throws Error if thrown by the underlying callable task.
* @throws RuntimeException if thrown by the underlying callable task,
* or the task throws a checked exception,
* or the task is interrupted (in this last case, it's the
* client's responsibility to process the cause of the
* exception).
* @see Callable#call()
*/
@Override
public V call() {
final RunnableFuture<V> newTask = new FutureTask<>(this.callable);
final Future<V> oldTask = this.fCachedValue.getAndUpdate(f -> {
/*
* If the atomic reference is un-initialised or reset,
* set it to the new task. Otherwise, return the
* previous (running or completed) task.
*/
return f == null ? newTask : f;
});
if (oldTask == null) {
/*
* Compute the new value on the current thread.
*/
newTask.run();
}
try {
return (oldTask == null ? newTask : oldTask).get();
} catch (final ExecutionException ee) {
/*
* Re-set the reference.
*/
this.fCachedValue.set(null);
final Throwable cause = ee.getCause();
if (cause instanceof Error) {
throw (Error) cause;
}
throw toUnchecked(cause);
} catch (final InterruptedException ie) {
/*
* Re-set the reference.
*/
this.fCachedValue.set(null);
/*
* It's the client's responsibility to check the cause.
*/
throw new RuntimeException(ie);
}
}
private static RuntimeException toUnchecked(final Throwable t) {
return t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t);
}
}
P. S. You might also want to take a look at the CompletableFuture
class.