Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't Java Concurrency In Practice listing 5.18 be done atomically with a lock?

In Java Concurrency in Practice, on page 106, it says " Memoizer3 is vulnerable to the problem [two threads seeing null and starting expensive computation] because a compound action (put-if-absent) is performed on the backing map that cannot be made atomic using locking." I don't understand why they say it cannot be made atomic using locking. Here is the original code:

package net.jcip.examples;

import java.util.*;
import java.util.concurrent.*;

/**
 * Memoizer3
 * <p/>
 * Memoizing wrapper using FutureTask
 *
 * @author Brian Goetz and Tim Peierls
 */
public class Memoizer3 <A, V> implements Computable<A, V> {
    private final Map<A, Future<V>> cache
        = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;

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

    public V compute(final A arg) throws InterruptedException {
        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 = ft;
            cache.put(arg, ft);
            ft.run(); // call to c.compute happens here
        }
        try {
            return f.get();
        } catch (ExecutionException e) {
            throw LaunderThrowable.launderThrowable(e.getCause());
        }
    }
}

Why wouldn't something like this work?

...
public V compute(final A arg) throws InterruptedException {
    Future<V> f = null;
    FutureTask<V> ft = null;
    synchronized(this){
        f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = new Callable<V>() {
                public V call() throws InterruptedException {
                    return c.compute(arg);
                }
             };
             ft = new FutureTask<V>(eval);
             f = ft;
             cache.put(arg, ft);                 
        }
    }
    if (f==ft) ft.run(); // call to c.compute happens here
    ...
like image 355
PeteyPabPro Avatar asked Nov 21 '13 00:11

PeteyPabPro


People also ask

Is Java Concurrency in Practice still valid?

TL: DR — Yes, Java Concurrency in Practice is still valid and one of the best books to learn Java multithreading and concurrency concepts.

What is concurrency in Java with example?

Concurrency is the ability to run several or multi programs or applications in parallel. The backbone of Java concurrency is threads (a lightweight process, which has its own files and stacks and can access the shared data from other threads in the same process).


1 Answers

It of course can be made atomic by using locking, imagine the most primitive case: you have a global lock around your entire function, then everything is single-threaded and therefore thread-safe. I assume that either they meant something else or there was a general misunderstanding.

Your code could even be improved by using the putIfAbsent method of ConcurrentHashMap like this:

public V compute(final A arg) throws InterruptedException {
  Future<V> f = cache.get(arg);
  if (f == null) {
    final Callable<V> eval = new Callable<V>() {
      public V call() throws InterruptedException {
        return c.compute(arg);
      }
    };
    final FutureTask<V> ft = new FutureTask<V>(eval);
    final Future<V> previousF = cache.putIfAbsent(arg, ft);
    if (previousF == null) {
      f = ft;
      ft.run(); 
    } else {
      f = previousF; // someone else will do the compute
    } 
  }
  return f.get();
}

f will at the end either be a previous value that has been added in between or the initial value, at the potential cost of an extra creation of a Callable, but no call to compute is done more than once.

like image 115
TwoThe Avatar answered Nov 15 '22 00:11

TwoThe