8

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

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. 我可以使用 a ConcurrentHashMap,但如果我很好理解,它只会使原子putget操作线程安全,即它不能确保buildB()对于给定值只调用一次方法。
  2. 我可以使用Collections.synchronizedMap(new HashMap<A, B>()),但我会遇到与第一点相同的问题。
  3. 我可以设置整个putIfNeededAndGet()方法synchronized,但是我可以让很多线程一起访问这个方法,所以它可能非常昂贵。
  4. 我可以使用双重检查锁定模式,但仍然存在相关的乱序写入问题

我还有什么其他解决方案?

我知道这是网络上一个相当普遍的话题,但我还没有找到一个清晰、完整且有效的示例。

4

4 回答 4

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;
}
于 2013-11-13T09:30:12.530 回答
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) {

        }
    }

}
于 2013-11-13T09:46:54.270 回答
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,那么任何其他调用者将只等待该线程。

于 2013-11-13T09:36:07.250 回答
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 :)

于 2016-03-22T18:58:08.157 回答