问题描述
这就是问题所在:我们想要一个其条目是线程安全的哈希表.
This is the problem: we want a hash table whose entries are thread-safe.
假设我有一个 <String, Long>
的哈希表,并且我想安全地增加其中一个条目线程的值:以下是否可以?:
Suppose I have a hash table of <String, Long>
, and I want to increase the value of one of the entries thread safely: is the following OK?:
HashMap<String , Long> hashTable = new HashMap<String, Long>();
然后每当我想增加一个条目时:
Then whenever I want to increase an entry:
Synchronized (hashTable.get("key"))
{
Long value = hashTable.get("key");
value++;
hashTable.put("key", value);
}
我认为它比 ConcurrentHashMap 更好,因为它只锁定一个条目,不像 ConcurrentHashMap 使用存储桶,并将一组条目锁定在一起.
I think it is better than ConcurrentHashMap, as it locks just one entry, unlike ConcurrentHashMap which uses buckets, and lock a group of entries together.
更重要的是,我不知道如何安全地使用 COncurrenHashMap 递增它.比如我认为下面的代码是不正确的:
More importantly, I don't know how to increment it using COncurrenHashMap safely. For example I think the following code is not correct:
ConcurrentHashMap<String , Long> hashTable = new ConcurrentHashMap<String, Long>();
Long value = hashTable.get("key");
value++;
hashTable.put("key", value);
我认为是不正确的,因为两个线程可以一个接一个地读取key,一个接一个地写入,最终得到一个错误的值.
I think it is not correct, because two threads can read the key one after another, and write one after another and end up in a wrong value.
你们怎么看?
推荐答案
你提出的方法不是线程安全的,因为初始的 hashTable.get()
操作——通过它你获取对象您打算同步的 - 相对于其他线程 put()
与同一键关联的值本身并不同步.此外,您的代码没有考虑将新值添加到映射或从映射中删除键的可能性(所谓的结构修改").如果有可能发生这种情况,无论键是什么,那么这些操作都必须与 所有 对地图的其他访问同步.
Your proposed approach is not thread-safe because the initial hashTable.get()
operation -- by which you obtain the object on which you intend to synchronize -- is not itself synchronized relative to other threads put()
ing a value associated with the same key. Moreover, your code does not account for the possibility of new values being added to the map or keys being removed from the map (so-called "structural modifications"). If ever that can happen, regardless of key, then those actions have to be synchronized with respect to all other accesses to the map.
您是对的,但是 ConcurrentHashMap
也不能解决这些问题.它提供的单个操作是线程安全的,其中包括 Map
本身未定义的一些操作,但 series 必须作为不间断单元执行的操作仍然需要通过同步来保护.
You are right, however, that ConcurrentHashMap
does not solve these problems either. It is thread-safe with respect to the individual operations it provides, which include some that Map
itself does not define, but series of operations that must be performed as an uninterrupted unit still need to be protected by synchronization.
我建议一种稍微不同的方法:使用 ConcurrentHashMap
和可变的 AtomicLong
作为值类型,而不是 Long
:
I suggest a slightly different approach: use a ConcurrentHashMap
with AtomicLong
, which is mutable, as your value type instead of Long
:
ConcurrentHashMap<String, AtomicLong> map;
然后,要更新键的值,即使您不确定该键在映射中是否已经有条目,您也可以这样做:
Then, to update the value for a key, even if you're not confident that the key already has an entry in the map, you do this:
AtomicLong value = map.putIfAbsent(key, new AtomicLong(0));
long updatedValue = value.incrementAndGet();
putIfAbsent()
确保值对象不会被相互冲突的 put 操作破坏.AtomicLong
的使用避免了多个操作被联合同步的需要,因为只需要一次映射访问——检索到的值被所有访问它的线程共享,并且本身可以被原子更新而无需进一步访问地图.
The putIfAbsent()
ensures that value objects are not clobbered by conflicting put operations. The use of AtomicLong
avoids the need for multiple operations to be jointly synchronized, because only one map access is needed -- the value retrieved is shared by all threads accessing it, and can itself be atomically updated without further accessing the map.
如果您可以确定该映射已经具有给定键的映射,那么您可以简单地这样做:
If you can be certain that the map already has a mapping for the given key, then you can simply do this:
AtomicLong value = map.get(key);
long updatedValue = value.incrementAndGet();
无论哪种方式,我认为这是您描述和暗示的操作所能做的最好的事情.
One way or the other, I think this is about the best you can do for the operations you describe and imply.
您甚至可以考虑像这样结合这两种方法:
You could even consider combining the two approaches like this:
AtomicLong value = map.get(key);
if (value == null) {
value = map.putIfAbsent(key, new AtomicLong(0));
}
long updatedValue = value.incrementAndGet();
这假设对于给定键没有映射的情况相对较少,并且在这种情况下避免创建新的 AtomicLong
.如果没有找到映射,则必须再次访问该映射,以确保存在映射并获取相应的值,但这里我们仍然需要 putIfAbsent()
来避免同步,因为有可能两个线程几乎同时尝试为同一个键添加映射.当需要添加新条目时,成本会更高,但平均而言,它可能会比我的第一个建议成本更低.然而,与任何性能问题一样,测试是必不可少的.
That supposes that it will be comparatively rare that there is not already a mapping for the given key, and it avoids creating a new AtomicLong
in that case. If no mapping is found then the map must be accessed a second time to ensure that there is a mapping and to get the corresponding value, but here we still need putIfAbsent()
if we want to avoid synchronization, because it is possible for two threads to both try to add a mapping for the same key, at about the same time. That's more costly when a new entry needs to be added, but it's possible that it would turn out to be less costly on average than my first suggestion. As with any performance question, however, it is essential to test.
这篇关于我们可以为每个条目使用 Synchronized 而不是 ConcurrentHashMap 吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!