为什么Java并发实践中的第5.18条不能使用锁原子化地完成?

7
在《Java并发实践》第106页中,它说:“Memoizer3容易受到问题的影响[两个线程看到null并开始昂贵的计算],因为在后台地图上执行了一个复合操作(放置-如果缺失),该操作无法使用锁进行原子化。”我不明白为什么他们会说它无法使用锁进行原子化。以下是原始代码:
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());
        }
    }
}

为什么这样的方法行不通呢?
...
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
    ...
1个回答

1
当然,使用锁可以使其原子化。想象一下最基本的情况:您在整个函数周围放置全局锁,那么一切都是单线程的,因此是线程安全的。我假设他们要么指的是其他事情,要么存在普遍误解。
您的代码甚至可以通过使用ConcurrentHashMap的putIfAbsent方法进行改进,像这样:
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 最终将成为在中间添加的先前值或初始值,可能会造成额外创建 Callable 的成本,但 compute 方法不会被调用超过一次。


是的,作者在文本中提供了一个 putIfAbsent 的解决方案。 - PeteyPabPro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接