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
...
TL: DR — Yes, Java Concurrency in Practice is still valid and one of the best books to learn Java multithreading and concurrency concepts.
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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With