WeakHashMap迭代和垃圾回收

16
我正在使用WeakHashMap来实现一个缓存。我想知道,如果我在遍历这个映射的键的同时,垃圾回收器正在主动从这个映射中删除键,我会收到ConcurrentModificationException吗?
我认为不会,因为据我所了解,并发修改异常是由于应用程序代码中的错误导致的,开发人员忘记了同一个映射被其他线程共享/使用的情况,而在这种情况下,不应该发生。但是我想知道,当WeakHashMap没有同步时,JVM会如何处理这种情况?

2
弱引用不适合用于缓存 - 它们可以立即清除,而不必等待垃圾回收器(虽然等待垃圾回收器也不是一个好策略)。 - Tom Hawtin - tackline
4个回答

15
正如bkail所说,当垃圾收集器从WeakHashMap中"删除"一个条目时,它不会导致并发修改。实际上,垃圾收集器通过存在于WeakReference对象(保存真正的键)本身的硬引用来收集底层对象。因此,直接被地图引用的真正对象(参考对象)不会被收集,因此地图在没有线程调用该地图的方法之前不会改变。那时,地图将检查来自GC的引用队列,并找到所有已收集的键,并从地图中删除它们-因此,地图结构的实际更改发生在您的一个线程上。
在考虑这一点时,可能会有一种情况,在这种情况下,您可能会在这种地图中得到一种并发修改,在另一种类型的映射中则不会-如果放置已经存在的键或调用getter方法。但是,在并发应用程序中,您应该在这些调用周围进行锁定,因此您的程序中可能存在真正的并发访问错误。
话虽如此,回答您的问题,您真的不应该使用WeakHashMap作为缓存(即使您正在谈论缓存键)。在缓存中,当值不再被引用时,您不希望它们“神奇地”消失。通常,您希望它们在达到某个最大值(类似于Apache集合中的LRUMap)或在内存需求释放时消失。

对于后者,您可以使用带有SoftReference的映射表(Apache集合提供了一个ReferenceMap,允许您指定键或值的引用类型)。软参考仅在内存压力下释放 - 另一方面,弱参考与GC识别到没有剩余硬参考的对象有关,并可以随时释放它。当然,软参考的实际工作方式也取决于JVM实现。

编辑:我重新阅读了您的问题并想解决另一点。由于实际修改发生在WeakHashMap的内部结构上,因此如果您只在单个线程中使用此映射表,则不需要同步任何方法调用。此行为与任何其他Map没有区别。


谢谢你,Kevin。非常好的解释,现在我非常清楚了。 - Shamik
是的,比我的回答更完整。+1。 - Brett Kail
  1. 如果迭代器在到达条目之前该条目消失了,会发生什么?next()方法是否不返回它?
  2. 如果我从entrySet()中检索一个条目,并且键在此后立即被GC回收,那么当我调用entry.getKey()时会发生什么?
- Monstieur
1
1-是的,内部迭代器会跳过任何已被垃圾回收的元素,并在确定下一个条目存在后保持下一个键的临时强引用,因此在检索它或前进到下一个元素之前,它不能被垃圾回收。 2-从entry Set返回的“Entry”是常规对象,它持有键/值的强引用,因此不会被垃圾回收。 - Kevin Brock

6

不会出现ConcurrentModificationException。当调用各种操作时,WeakHashMap使用ReferenceQueue.poll。换句话说,每个调用者都要默默地负责清除Map中过期的条目。但是,这意味着从多个线程调用WeakHashMap的方法可能不安全,即使这些方法在表面上看起来是“只读”的,因为任何对get()的调用都可能破坏另一个线程正在尝试迭代的条目链表。


0

WeakHashMap 对键进行弱引用,而不是对值进行弱引用,因此如果您想在值未被使用时释放空间,则不适合用作值的缓存。您可能需要查看 MapMaker 来自 google collections


1
这是真正而好的建议,但它并没有回答问题。 - luke
1
@luke 我认为两种答案都是有效的。是的,了解在 WeakHashMap 上进行迭代会做什么是很好的,但如果在问题的其他上下文中 WeakHashMap 是错误的方法,则指出这一点也是合理的答案。话虽如此,“缓存”这个词不足以知道是否适合使用 WeakHashMap。WeakHashMap 具有弱键。如果要缓存一个参数为映射键的计算(并且该键使用标识相等性),并且您希望结果具有与键相同的生命周期,则可以使用它。 - Laurence Gonsalves

-2

文档对此并不十分清晰,但确实提到了这一点:

WeakHashMap类的行为部分取决于垃圾回收器的操作,因此对于这个类,一些熟悉的(尽管不是必需的)Map不变量不成立。由于垃圾回收器可能随时丢弃键,因此WeakHashMap可能会表现得好像一个未知的线程正在默默地删除条目。特别是,即使在一个WeakHashMap实例上进行同步并且不调用其任何修改器方法,size方法返回的值可能随时间减小,isEmpty方法可能先返回false然后返回true,containsKey方法可能针对给定的键先返回true然后返回false,get方法针对给定的键可能返回一个值,但后来返回null,put方法可能返回null,而remove方法可能返回false,对于先前似乎存在于映射中的键。对键集、值集和条目集的连续检查可能产生逐渐减少的元素数量。- Java API

我认为,考虑到这个描述,你应该预期在迭代地图时偶尔会收到ConcurrentModificationException。我建议你设计你的缓存,尽可能少地进行迭代。


是的,这正是我担心的。虽然我并没有在迭代它,但我想添加一些调试功能来检查缓存中的内容,就是在那个时候我产生了这个疑问。 - Shamik
如果仅用于调试目的,您可以复制密钥集,然后检查它。 - luke
1
Luke,“silent thread”并不是真正的线程,因此只要GC标记(而不是实际删除)要删除的条目,就不会有并发修改。我在并发环境中使用WeakHashMap(不是用于缓存),并没有遇到这样的问题。 - Kevin Brock

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接