9

回到并发。现在很明显,要使double checked locking变量工作,需要将变量声明为volatile. 但是,如果如下使用双重检查锁定会怎样。

class Test<A, B> {

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

    public B fetch(A key, Function<A, B> loader) {
        B value = map.get(key);
        if (value == null) {
            synchronized (this) {
                value = map.get(key);
                if (value == null) {
                    value = loader.apply(key);
                    map.put(key, value);
                }
            }
        }
        return value;
    }

}

为什么它真的必须是ConcurrentHashMap而不是常规的HashMap?所有地图修改都在synchronized块内完成,代码不使用迭代器,因此从技术上讲不应该存在“并发修改”问题。

请避免建议使用putIfAbsent/computeIfAbsent因为我在询问这个概念而不是使用 API :) 除非使用此 API 有助于HashMapvsConcurrentHashMap主题。

2016-12-30 更新

Holger 在下面的评论中回答了这个问题“HashMap.get不会修改结构,但您的调用会修改put。由于get在同步块之外调用,它可以看到put同时发生的操作的不完整状态。” 谢谢!

4

1 回答 1

17

这个问题在很多方面都很混乱,很难回答。

如果仅从单个线程调用此代码,那么您将使其过于复杂;你不需要任何同步。但显然这不是你的意图。

因此,多个线程将调用 fetch 方法,该方法在没有任何同步的情况下委托给 HashMap.get()。HashMap 不是线程安全的。巴姆,故事结束。即使您尝试模拟双重检查锁定也没关系;现实情况是,在 map 上调用get()andput()将操纵 的内部可变数据结构HashMap,而所有代码路径上没有一致的同步,并且由于您可以从多个线程同时调用这些,因此您已经死了。

(另外,你可能认为这HashMap.get()是一个纯粹的读操作,但那也是错误的。如果 HashMap 实际上是一个 LinkedHashMap(它是 HashMap 的一个子类。) LinkedHashMap.get() 将更新访问顺序,这涉及到写入内部数据结构——在这里,没有同步的并发。但即使 get() 不写,你的代码仍然是坏的。)

经验法则:当你认为你有一个巧妙的技巧可以让你避免同步时,你几乎肯定是错的。

于 2015-10-26T16:45:08.897 回答