使用常规HashMap双重检查锁定

oᴉɹ*_*ǝɥɔ 6 java concurrency hashmap concurrenthashmap double-checked-locking

回到并发.到目前为止,很明显,为了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;
    }

}
Run Code Online (Sandbox Code Playgroud)

为什么它必须是ConcurrentHashMap而不是常规的HashMap?所有映射修改都在synchronized块内完成,代码不使用迭代器,因此从技术上讲,应该没有"并发修改"问题.

请避免建议使用putIfAbsent/,computeIfAbsent因为我询问的概念而不是API的使用:)除非使用此API有助于HashMapvs ConcurrentHashMapsubject.

更新2016-12-30

这个问题由Holger下面的评论回答" HashMap.get不修改结构,但你的调用put确实如此.由于调用get了synchronized块的外部,它可以看到put同时发生的操作的不完整状态." 谢谢!

Bri*_*etz 14

这个问题混淆了很多,很难回答.

如果只从一个线程调用此代码,那么你就太复杂了; 你不需要任何同步.但显然这不是你的意图.

因此,多个线程将调用fetch方法,该方法在没有任何同步的情况下委托给HashMap.get().HashMap不是线程安全的.巴姆,故事的结尾.如果您正在尝试模拟双重检查锁定,则无关紧要; 实际情况是,调用get()put()在地图上将操纵内部可变数据结构,HashMap在所有代码路径上没有一致的同步,并且因为你可以从多个线程同时调用它们,所以你已经死了.

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

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

  • @cherio:`HashMap.get`不会修改结构,但是你对`put`的调用会这样做.由于在`synchronized`块之外有一个`get`的调用,它可以看到一个'put`操作同时发生的不完整状态.这种错误的读取可能会导致许多违反直觉的行为. (2认同)