Search code examples
javaconcurrencythread-safetyjava.util.concurrentconcurrenthashmap

In Java Concurrency In Practice by Brian Goetz, why if (f == null) was checked twice in Memoizer


Java Concurrency In Practice by Brian Goetz provides an example of a efficient scalable cache for concurrent use. The final version of the example showing the implementation for class Memoizer (pg 108) shows such a cache. I am wondering why there were an inner and outer check of the if (f == null). The second one does not make any sense because:

  1. there is a check ahead and the immediate last step ahead will definitely return a not-null value out of cache.putIfAbsent(arg, ft);
  2. the ft.run() inside the second check does not make any sense because f.get() will be called immediately thereafter.

Here is the code for Memoizer:

public class Memoizer<A, V> implements Computable<A, V> {
private final ConcurrentMap<A, Future<V>> cache
    = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;

public Memoizer(Computable<A, V> c) { this.c = c; }

public V compute(final A arg) throws InterruptedException {
    while (true) {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = new Callable<V>() {
                public V call() throws InterruptedException {
                    return c.compute(arg);
                }
            };
            FutureTask<V> ft = new FutureTask<V>(eval);
            f = cache.putIfAbsent(arg, ft);
            if (f == null) { f = ft; ft.run(); }
        }
        try {
            return f.get();
        } catch (CancellationException e) {
            cache.remove(arg, f);
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }
}

Solution

    1. there is a check ahead and the immediate last step ahead will definitely return a not-null value out of cache.putIfAbsent(arg, ft);

    If there is only a single thread calling compute, then cache.putIfAbsent(arg, ft); will always return null, as there is no previous value.

    If there are two or more threads calling the compute method at the same time, then only one of them will get null out of cache.putIfAbsent(arg, ft);, the others will get the value of ft that the thread which got null created.

    In that case, the other threads throw away their instance of FutureTask and continue with the instance that they received from cache.putIfAbsent(arg, ft);

    1. the ft.run() inside the second check does not make any sense because f.get() will be called immediately thereafter.

    You need to run a FutureTask in order to get the value from it later. If you don't call run you won't ever get a value. The thread that created the FutureTask that got stored in the cache, will run it and then get will return immediately, because it is already complete at that point.

    But the other threads that called compute at the same time and that got a non-null value from putIfAbsent, will go to the get call and wait until the first thread is done with the run method.