仅在尚未存在时创建并放置映射值,并获取它:线程安全实现

时间:2013-11-13 09:14:00

标签: java multithreading map

使此代码段线程安全的最佳方法是什么?

private static final Map<A, B> MAP = new HashMap<A, B>();

public static B putIfNeededAndGet(A key) {
    B value = MAP.get(key);
    if (value == null) {
        value = buildB(...);
        MAP.put(key, value);
    }
    return value;
}

private static B buildB(...) {
    // business, can be quite long
}

以下是我能想到的几个解决方案:

  1. 我可以使用ConcurrentHashMap,但如果我很好理解,只是使原子putget操作成为线程安全的,即它不确保仅为给定值调用buildB()方法一次。
  2. 我可以使用Collections.synchronizedMap(new HashMap<A, B>()),但我会遇到与第一点相同的问题。
  3. 我可以设置整个putIfNeededAndGet()方法synchronized,但我可以让很多线程一起访问这个方法,所以它可能非常昂贵。
  4. 我可以使用双重检查锁定模式,但仍然存在相关的out-of-order writes issue
  5. 我可以提供哪些其他解决方案?

    我知道这是网络上一个非常常见的话题,但我还没有找到一个清晰,完整且有效的例子。

4 个答案:

答案 0 :(得分:5)

使用ConcurrentHashMap和您使用的延迟初始化模式

public static B putIfNeededAndGet(A key) {
    B value = map.get(key);
    if (value == null) {
        value = buildB(...);
        B oldValue = map.putIfAbsent(key, value);
        if (oldValue != null) {
             value = oldValue;
        }
    }
    return value;
}

答案 1 :(得分:2)

这可能不是您正在寻找的答案,但使用Guava CacheBuilder,它已经完成了所有这些以及更多:

private static final LoadingCache<A, B> CACHE = CacheBuilder.newBuilder()
   .maximumSize(100) // if necessary
   .build(
       new CacheLoader<A, B>() {
         public B load(A key) {
           return buildB(key);
         }
       });

您还可以轻松添加定时到期和其他功能。

此缓存将确保load()(或在您的情况下为buildB)不会与同一key同时调用。如果一个线程已经构建了B,那么任何其他调用者都将等待该线程。

答案 2 :(得分:2)

在上面的解决方案中,许多线程可能会同时归类processB(...),因此所有线程都会计算出来。但在我的情况下,我使用的是Future,而单个线程只将旧值作为null获取,因此它只计算processB其余值将等待f.get()

 private static final ConcurrentMap<A, Future<B>> map = new ConcurrentHashMap<A, Future<B>>();
public static B putIfNeededAndGet(A key) {
    while (true) {
        Future<V> f = map.get(key);
        if (f == null) {
            Callable<B> eval = new Callable<V>() {
                public B call() throws InterruptedException {
                    return buildB(...);
                }
            };
            FutureTask<V> ft = new FutureTask<V>(eval);
            f = map.putIfAbsent(arg, ft);
            if (f == null) {
                f = ft;
                ft.run();
            }
        }
        try {
            return f.get();
        } catch (CancellationException e) {
            cache.remove(arg, f);
        } catch (ExecutionException e) {

        }
    }

}

答案 3 :(得分:1)

想想也许这对其他人也有用,使用java 8 lambdas我创建了这个对我有用的功能:

private <T> T getOrCreate(Object key, Map<Object, T> map,
                                Function<Object, T> creationFunction) {
    T value = map.get(key);

    // if the doesn't exist yet - create and add it
    if (value == null) {
        value = creationFunction.apply(key);
        map.put(label, metric);
    }
    return value;
}

然后你可以像这样使用它:

Object o = getOrCreate(key, map, s -> createSpecialObjectWithKey(key));

我为特定的东西创建了这个,但是将上下文和代码更改为更一般的外观,这就是为什么我的creationFunction有一个参数,它也可以没有参数......

你也可以通过将Object更改为泛型类型来更多地通用它,如果不清楚,请告诉我,我将添加另一个示例。

更新:

我刚刚发现了Map.computeIfAbsent,基本上也是这样,得爱java 8 :)