【问题标题】:Cache using ConcurrentHashMap使用 ConcurrentHashMap 进行缓存
【发布时间】:2015-01-10 23:08:31
【问题描述】:

我有以下代码:

public class Cache {

    private final Map map = new ConcurrentHashMap();

    public Object get(Object key) {

        Object value = map.get(key);
        if (value == null) {
            value = new SomeObject();
            map.put(key, value);
        }

        return value;
    }
}

我的问题是: 映射的put 和get 方法是线程安全的,但是由于整个块不同步——多个线程可以添加相同的键两次吗?

【问题讨论】:

  • @Mureinik 的回答是一个更一般原则的具体示例:完全用线程安全组件构建系统不会使系统作为一个整体线程安全。

标签: java multithreading collections concurrency hashmap


【解决方案1】:

put 和 get 是线程安全的,因为从不同的线程调用它们不会破坏数据结构(例如,使用普通的 java.util.HashMap 是可能的)。

但是,由于块不同步,您可能仍然有多个线程添加相同的键: 两个线程都可能通过null 检查,一个线程添加键并返回其值,然后第二个线程将覆盖该值并返回一个新值。

【讨论】:

    【解决方案2】:

    多个线程可以添加同一个键两次吗?

    是的,他们可以。要解决此问题,您可以:

    1) 使用putIfAbsent 方法而不是put。它可以创建非常快但不必要的 SomeObject 实例。

    2) 使用双重检查锁定:

    Object value = map.get(key);
    if (value == null) {
        synchronized (map) {
            value = map.get(key);
            if (value == null) {
                value = new SomeObject();
                map.put(key, value);
            }
        }
    }
    return value;
    

    锁的速度要慢得多,但只会创建必要的对象

    【讨论】:

      【解决方案3】:

      您还可以将检查和 putIfAbsent 结合起来,例如:

      Object value = map.get(key);
      if (value == null) {
          map.putIfAbsent(key, new SomeObject());
      }
      return value;
      

      从而将不必要的新对象减少到在检查和 putIfAbsent 之间的短时间内引入新条目的情况。

      如果您感觉幸运并且读取的次数远远多于写入地图的次数,您还可以创建自己的写入时复制地图,类似于 CopyOnWriteArrayList。

      【讨论】:

      • 缓存未命中时返回null。
      【解决方案4】:

      从 Java 8 开始,您还可以通过以下方式防止添加重复键:

      public class Cache {
      
          private final Map map = new ConcurrentHashMap();
      
          public Object get(Object key) {
      
              Object value = map.computeIfAbsent(key, (key) -> {
                return new SomeObject();
              });
      
              return value;
          }
      }
      

      API docs 状态:

      如果指定的键尚未与值关联,则尝试 使用给定的映射函数计算其值并输入 进入这张地图,除非为空。 执行整个方法调用 原子地,因此每个键最多应用一次该函数。一些 其他线程在此地图上尝试的更新操作可能是 在计算过程中被阻塞,所以计算应该是 简短而简单,并且不得尝试更新任何其他映射 这张地图。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-02-25
        • 2015-04-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多