哈希表并发问题

35

为了提高速度,我有一个Hashmap,希望不需要加锁。假设我不介意过期数据,同时更新和访问它是否会引起任何问题?

我的访问是获取操作,而不是遍历,删除操作也是更新的一部分。


你的更新操作包括删除吗?你的访问操作包括迭代吗? - Michael Myers
没关系,我猜那些问题是无关紧要的。 - Michael Myers
他们是这样吗?【需要更多睡眠】 - Michael Myers
9个回答

61

是的,这会造成重大问题。一个例子就是在将值添加到哈希映射时可能会发生什么:这可能会导致表重新哈希化,如果在另一个线程正在遍历碰撞列表(哈希表“桶”)时发生了这种情况,那个线程可能会错误地找不到在映射中存在的键。

HashMap 明确不适合并发使用。

应该使用 ConcurrentHashMap 代替。


17

同步或使用ConcurrentHashMap的重要性不容小觑。

直到几年前,我错误地认为只同步HashMap上的put和remove操作就足够了。这当然是非常危险的,在某些jdk版本(早期的1.5版本)中实际上会导致HashMap.get()进入无限循环。

我在几年前所做的事情(而真正不应该做的):

public MyCache {
    private Map<String,Object> map = new HashMap<String,Object>();

    public synchronzied put(String key, Object value){
        map.put(key,value);
    }

    public Object get(String key){
        // can cause in an infinite loop in some JDKs!!
        return map.get(key);
    }
}

编辑:我想加一个例子,说明不要做什么(请参见上文)


4
我认为无限循环是每个Sun JDK的特色。我记不清是哪个,但一款知名的开源软件在日志记录时使用了HashMap,由于不需要完全准确性而将其留作未同步状态以提高速度。在生产环境中,偶尔会陷入无限循环,这比抛出未经检查的异常更糟糕。 - Tom Hawtin - tackline
1
我亲眼见过这种情况。如果你尝试使用“我不关心数据损坏”的方法在多个线程中更新HashMap,它会挂起你的JVM。未同步/非线程安全的HashMap只有在没有更新/删除或只有一个线程访问它时才是安全的。 - Peter Lawrey

12

当你有疑问时,要查看类的Javadocs

请注意,此实现未同步。如果多个线程同时访问哈希映射,并且其中至少一个线程在结构上修改了映射,则必须通过外部同步来进行同步。(结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已经包含的键相关联的值不是结构修改。)这通常是通过在自然封装映射的某个对象上同步完成的。如果没有这样的对象,则应使用Collections.synchronizedMap方法“包装”该映射。最好在创建时进行此操作,以防止意外的非同步访问映射:

Map m = Collections.synchronizedMap(new HashMap(...));

(强调不是我)

因此,根据您说的线程将从Map中删除映射的事实,答案是是的它肯定会引起问题并且是绝对不安全的。


11

我不建议使用NonBlockingHashMap。只需使用ConcurrentHashMap即可。 - Johnny

8

你描述的条件无法满足HashMap。由于更新映射的过程不是原子性的,因此可能会遇到无效状态的映射。多次写入可能会导致其处于损坏状态。ConcurrentHashMap(1.5或更高版本)可以实现你想要的功能。


4
如果你说的“同时”是指来自多个线程,那么是的,你需要锁定对它的访问(或者使用ConcurrentHashMap或类似工具来为你做锁操作)。

0

我在这里或其他地方读到,不,你不能从多线程访问,但没有人说发生了什么。

所以,今天我看到了(这就是我在这个老问题上的原因),一个自三月份以来一直在生产中运行的应用程序:将2个放在相同的HashSet(然后是HashMap)上会导致CPU超载(接近100%),内存增加3GB,然后因为垃圾回收而下降。 我们不得不重新启动应用程序。


0

不会有任何问题,只要您按照以下步骤操作:

  1. 在任何多线程发生之前,在单个线程的第一次加载时将数据放入HashMap中。这是因为添加数据的过程会改变modcount,并且在第一次添加数据时(将返回null)与替换数据时(旧数据将被返回,但modcount不会被改变)是不同的。Modcount是使迭代器快速失败的原因。但如果您使用get,则不会进行任何迭代,所以没问题。

  2. 在整个应用程序中使用相同的键。一旦应用程序启动并加载其数据,就不能将其他键分配给此映射。这样,get将获取过期数据或插入新数据的数据 - 不会出现任何问题。


0

像其他人提到的那样,使用ConcurrentHashMap或在更新时同步地图。


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