【问题标题】:Volatile HashMap vs ConcurrentHashMapVolatile HashMap vs ConcurrentHashMap
【发布时间】:2012-05-08 15:01:45
【问题描述】:

我有一个缓存类,其中包含一个 volatile HashMap<T> 来存储缓存项。

我很好奇将volatile HashMap 更改为ConcurrentHashMap 会有什么后果?

我会获得性能提升吗?此缓存为只读缓存。

最好的选择是什么?只是HashMap?缓存正在按时间间隔填充。

【问题讨论】:

  • 如果它是只读的,你不需要……也不需要。
  • volatile on a HashMap 意味着当您获取/设置 HashMap 对象时,您跨越了内存屏障。当您在地图中添加或删除东西时,它什么也不做
  • @BrianRoach 你会用什么?

标签: java caching concurrency hashmap


【解决方案1】:

首先,您似乎不了解volatile 关键字的作用。它确保如果声明为volatile 的变量所持有的引用值 发生变化,其他线程将看到它而不是缓存副本。它与访问HashMap的线程安全无关

鉴于此,并且您说HashMap 是只读的……您当然不需要使用任何提供线程安全的东西,包括ConcurrentHashMap

编辑添加:您的最后一次编辑现在说“缓存正在按时间间隔填充”

那不是只读的,是吗?

如果你要让线程读取它你正在写(更新现有的HashMap)那么你应该使用ConcurrentHashMap,是的。

如果您要填充一个全新的 HashMap,然后将其分配给现有变量,则使用 volatile

【讨论】:

  • 这就是我使用 volatile 的原因,有一个后台线程从文件中读取并创建一个新的 hashmap 然后分配给缓存。我打算改变设计,这就是我问的原因。不过,你回答了这两种情况。谢谢。
  • 实际上您可以使用volatile 来确保人们看到最新的元素(基本上与获取“易失性”数组元素的方式相同),这只是a)性能不佳,b)令人费解,c)在添加数据时对内部竞争条件没有帮助,d)很愚蠢。但是可行! ;)
  • @Voo 你会用什么?或者你会怎么做?
  • @Darth 注意我的评论是关于使用 volatile 在哈希图上添加条目并确保它们可见——而不是你想要做的。 b2t:我会先选择ConcurrentHashmap - 它就是为此目的而构建的。如果我在映射中遇到高性能瓶颈,我会选择 Cliff 的无锁 HashMap 实现,无论如何使用后台线程从缓存中删除旧条目。更少的内存开销,没有大的尖峰并且仍然相当不错的性能。如果您真的只想每 X 分钟创建一个新缓存,Michael 的解决方案可以正常工作。
  • @BrianRoach 我没有得到您的评论,即只读地图不需要是线程安全的。某些线程必须在某个时间点将一些数据放入地图中,如果在地图传递给其他线程之后添加了这些数据,那么您就有麻烦了。
【解决方案2】:

你说缓存是只读的,但也会在一个看似矛盾的时间间隔内更新。

如果整个缓存定期更新,我会继续使用 volatile。 volatile 将确保更新后的地图安全发布。

public final class  Cache
{
   private volatile Map<?,?> cache;

   private void mapUpdate() {
      Map<?,?> newCache = new HashMap<>();

      // populate the map

      // update the reference with an immutable collection
      cache = Collections.unmodifiableMap(newCache);
   }
}

如果间隔更新是修改同一个缓存,那么你可能想要使用 ConcurrentHashMap,或者复制地图,更新副本,更新引用。

public final class  Cache
{
   private volatile Map<?,?> cache;

   private void mapUpdate() {
      Map<?,?> newCache = new HashMap<>(cache);

      // update the map

      // update the reference with an immutable collection
      cache = Collections.unmodifiableMap(newCache);
   }
}

【讨论】:

  • 我会考虑保持缓存字段非易失性。由于 Collections.unmodifiableMap 最终字段链接到新版本地图的链接将被安全发布。在某些时候,所有线程都会获取新版本的缓存映射。通常(但并非总是如此!)缓存是否不会立即可用但在几毫秒内可用并不重要。并且使这个字段非易失性提高了 CPU 缓存性能。
【解决方案3】:

我的 Web 应用程序有一个类似的用例。我正在为我的内存缓存使用 HashMap。用例如下-

  1. 一个用户请求进来,首先使用输入键检查缓存是否存在记录。这是在 add 方法中完成的。
  2. 如果对象不存在,则将新记录插入缓存中。
  3. 类似地,在 remove 方法中,首先使用键检查缓存中是否存在记录,如果找到则将其删除。

我想确保两个线程同时执行一个 on add 和另一个 on remove 方法,这种方法是否可以确保他们在他们看到缓存中的最新数据?如果我没记错的话,同步方法负责线程安全,而 volatile 负责可见性。

private volatile HashMap<String,String> activeRequests = new HashMap<String,String>();
public synchronized boolean add(String pageKey, String space, String pageName) {
    if (!(activeRequests.get(pageKey) == null)) {
       return false;
    }
    activeRequests.put(pageKey, space + ":" + pageName);
    return true;
}

public synchronized void remove(String pageKey) {       
    if(!(activeRequests.get(pageKey) == null))
        activeRequests.remove(pageKey);
    }

【讨论】:

  • 如果 activeRequests 映射引用保持不变并且没有变化并且只有映射内部的值发生变化,为什么您需要 volatile ?假设这是一个由多个线程共享的单例实例。您应该使用 concurrentHashMap 而不是同步方法,因为您不会在此处更改任何其他内容。
【解决方案4】:

AFAIK,尽管 first answer explains correctly,根据用例,在缓存上使用 volatile 频繁刷新和替换是不必要的开销,实际上可能是坏的或不一致的,假设这只是静态的元数据快照,不被其他线程更新。

如果以 Http Request 为例,它从缓存中读取所有内容以获取所需的所有内容,该请求使用映射的引用,然后开始从引用中读取一些键,然后在读取中途,缓存引用为更新到新的哈希图(刷新),现在它开始读取不同的缓存状态,如果缓存中的条目不是针对特定时间快照T,则可能会变得不一致。使用 volatile,您在 T1 读取 Key1:Val1,在 T2 读取 Key2:Val2,而您需要在 T1 读取同一快照的 Val1、Val2。使用 volatile 时,您的参考始终会更新,您可以第一次读取 Key1:Val1,第二次读取 Key1:Val2 在同一请求中给出不同的数据。

如果没有 volatile,请求将使用始终指向引用快照的引用,直到完成处理。如果没有 volatile,您将始终在 T1 读取 Key1:Val1,在 T2 读取相同的值 Key2:Val1。使用此引用的所有请求都完成后,旧的取消引用映射将被 GCed。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-25
    • 2010-11-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多