迭代 ConcurrentHashMap 值线程安全吗?
Posted
技术标签:
【中文标题】迭代 ConcurrentHashMap 值线程安全吗?【英文标题】:Is iterating ConcurrentHashMap values thread safe? 【发布时间】:2011-04-15 16:13:53 【问题描述】:在 ConcurrentHashMap 的 javadoc 中如下:
检索操作(包括 get)一般不会阻塞,因此可能与更新操作(包括 put 和 remove)重叠。检索反映了最近完成的更新操作在其开始时保持的结果。对于 putAll 和 clear 等聚合操作,并发检索可能仅反映插入或删除某些条目。类似地,迭代器和枚举返回反映哈希表在创建迭代器/枚举时或之后的某个时间点的状态的元素。它们不会抛出 ConcurrentModificationException。 但是,迭代器被设计为一次只能由一个线程使用。
这是什么意思?如果我尝试同时用两个线程迭代映射会发生什么?如果我在迭代时从映射中放置或删除一个值会发生什么?
【问题讨论】:
【参考方案1】:什么意思?
这意味着你不应该尝试在两个线程中使用相同的迭代器。如果您有两个线程需要迭代键、值或条目,那么它们都应该创建并使用自己的迭代器。
如果我尝试同时使用两个线程迭代映射会发生什么?
如果您违反此规则会发生什么,目前尚不完全清楚。您可能会遇到令人困惑的行为,就像(例如)两个线程尝试从标准输入读取而不进行同步一样。你也可能得到非线程安全的行为。
但是如果两个线程使用不同的迭代器,你应该没问题。
如果我在迭代时从映射中放置或删除一个值会发生什么?
如果两个线程使用相同的迭代器:见上文。您可能会感到困惑并且可能是非线程安全的行为。
如果线程使用不同的迭代器,您引用的 javadoc 部分可以充分回答它。基本上,没有定义一个线程/迭代器是否会看到另一个线程/迭代器进行的任何并发插入、更新或删除的影响。但是,插入/更新/删除将根据地图的并发属性进行。
【讨论】:
【参考方案2】:什么意思?
这意味着您从ConcurrentHashMap
获得的每个迭代器都设计为由单个线程使用,不应被传递。这包括 for-each 循环提供的语法糖。
如果我尝试同时使用两个线程迭代映射会发生什么?
如果每个线程都使用它自己的迭代器,它将按预期工作。
如果我在迭代时从映射中放置或删除一个值会发生什么?
如果您这样做,可以保证事情不会中断(这是ConcurrentHashMap
中“并发”的一部分含义)。但是,不能保证一个线程会看到另一个线程执行的对映射的更改(没有从映射中获取新的迭代器)。迭代器保证在创建地图时反映地图的状态。进一步的变化可能会反映在迭代器中,但不一定非得如此。
总之,这样的陈述
for (Object o : someConcurrentHashMap.entrySet())
// ...
几乎每次你看到它都会很好(或至少是安全的)。
【讨论】:
那么如果在迭代过程中,另一个线程从地图中删除了一个对象 o10 会发生什么?即使它已被删除,我还能在迭代中看到 o10 吗? @Waldheinz 如上所述,实际上并没有指定现有迭代器是否会反映以后对地图的更改。所以我不知道,而且按照规范没有人这样做(不查看代码,并且可能会随着运行时的每次更新而改变)。所以你不能依赖它。 但是我在迭代ConcurrentHashMap
时仍然得到ConcurrentModificationException
,为什么?
@KimiChiu 您可能应该发布一个新问题,提供触发该异常的代码,但我高度怀疑它直接源于迭代并发容器。除非 Java 实现有问题。【参考方案3】:
你可以使用这个类来测试两个访问线程和一个改变ConcurrentHashMap
的共享实例:
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentMapIteration
private final Map<String, String> map = new ConcurrentHashMap<String, String>();
private final static int MAP_SIZE = 100000;
public static void main(String[] args)
new ConcurrentMapIteration().run();
public ConcurrentMapIteration()
for (int i = 0; i < MAP_SIZE; i++)
map.put("key" + i, UUID.randomUUID().toString());
private final ExecutorService executor = Executors.newCachedThreadPool();
private final class Accessor implements Runnable
private final Map<String, String> map;
public Accessor(Map<String, String> map)
this.map = map;
@Override
public void run()
for (Map.Entry<String, String> entry : this.map.entrySet())
System.out.println(
Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']'
);
private final class Mutator implements Runnable
private final Map<String, String> map;
private final Random random = new Random();
public Mutator(Map<String, String> map)
this.map = map;
@Override
public void run()
for (int i = 0; i < 100; i++)
this.map.remove("key" + random.nextInt(MAP_SIZE));
this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
System.out.println(Thread.currentThread().getName() + ": " + i);
private void run()
Accessor a1 = new Accessor(this.map);
Accessor a2 = new Accessor(this.map);
Mutator m = new Mutator(this.map);
executor.execute(a1);
executor.execute(m);
executor.execute(a2);
不会抛出异常。
在访问线程之间共享相同的迭代器会导致死锁:
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentMapIteration
private final Map<String, String> map = new ConcurrentHashMap<String, String>();
private final Iterator<Map.Entry<String, String>> iterator;
private final static int MAP_SIZE = 100000;
public static void main(String[] args)
new ConcurrentMapIteration().run();
public ConcurrentMapIteration()
for (int i = 0; i < MAP_SIZE; i++)
map.put("key" + i, UUID.randomUUID().toString());
this.iterator = this.map.entrySet().iterator();
private final ExecutorService executor = Executors.newCachedThreadPool();
private final class Accessor implements Runnable
private final Iterator<Map.Entry<String, String>> iterator;
public Accessor(Iterator<Map.Entry<String, String>> iterator)
this.iterator = iterator;
@Override
public void run()
while(iterator.hasNext())
Map.Entry<String, String> entry = iterator.next();
try
String st = Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
catch (Exception e)
e.printStackTrace();
private final class Mutator implements Runnable
private final Map<String, String> map;
private final Random random = new Random();
public Mutator(Map<String, String> map)
this.map = map;
@Override
public void run()
for (int i = 0; i < 100; i++)
this.map.remove("key" + random.nextInt(MAP_SIZE));
this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
private void run()
Accessor a1 = new Accessor(this.iterator);
Accessor a2 = new Accessor(this.iterator);
Mutator m = new Mutator(this.map);
executor.execute(a1);
executor.execute(m);
executor.execute(a2);
一旦您开始在访问器和修改器线程之间共享相同的Iterator<Map.Entry<String, String>>
,java.lang.IllegalStateException
s 就会开始弹出。
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentMapIteration
private final Map<String, String> map = new ConcurrentHashMap<String, String>();
private final Iterator<Map.Entry<String, String>> iterator;
private final static int MAP_SIZE = 100000;
public static void main(String[] args)
new ConcurrentMapIteration().run();
public ConcurrentMapIteration()
for (int i = 0; i < MAP_SIZE; i++)
map.put("key" + i, UUID.randomUUID().toString());
this.iterator = this.map.entrySet().iterator();
private final ExecutorService executor = Executors.newCachedThreadPool();
private final class Accessor implements Runnable
private final Iterator<Map.Entry<String, String>> iterator;
public Accessor(Iterator<Map.Entry<String, String>> iterator)
this.iterator = iterator;
@Override
public void run()
while (iterator.hasNext())
Map.Entry<String, String> entry = iterator.next();
try
String st =
Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
catch (Exception e)
e.printStackTrace();
private final class Mutator implements Runnable
private final Random random = new Random();
private final Iterator<Map.Entry<String, String>> iterator;
private final Map<String, String> map;
public Mutator(Map<String, String> map, Iterator<Map.Entry<String, String>> iterator)
this.map = map;
this.iterator = iterator;
@Override
public void run()
while (iterator.hasNext())
try
iterator.remove();
this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
catch (Exception ex)
ex.printStackTrace();
private void run()
Accessor a1 = new Accessor(this.iterator);
Accessor a2 = new Accessor(this.iterator);
Mutator m = new Mutator(map, this.iterator);
executor.execute(a1);
executor.execute(m);
executor.execute(a2);
【讨论】:
您确定“在访问线程之间共享相同的迭代器会导致死锁”吗?该文件说读取没有被阻止,我尝试了你的程序,还没有发生死锁。虽然迭代结果会出错。【参考方案4】:This 可能会给你一个很好的见解
ConcurrentHashMap 通过稍微放宽它对调用者的承诺来实现更高的并发性。检索操作将返回由最近完成的插入操作插入的值,也可能返回由同时进行的插入操作添加的值(但在任何情况下都不会返回无意义的结果)。 由 ConcurrentHashMap.iterator() 返回的迭代器将最多返回每个元素一次,并且永远不会抛出 ConcurrentModificationException,但可能会或可能不会反映自构造迭代器以来发生的插入或删除。在迭代集合时,不需要(甚至不可能)表范围的锁定来提供线程安全。在任何不依赖锁定整个表以防止更新的能力的应用程序中,ConcurrentHashMap 都可以用作 synchronizedMap 或 Hashtable 的替代品。
关于这个:
但是,迭代器被设计为一次只能由一个线程使用。
这意味着,虽然在两个线程中使用 ConcurrentHashMap 生成的迭代器是安全的,但它可能会导致应用程序中出现意外结果。
【讨论】:
【参考方案5】:这意味着你不应该在多个线程之间共享一个迭代器对象。创建多个迭代器并在单独的线程中同时使用它们很好。
【讨论】:
有什么原因你没有在迭代器中将 I 大写?由于它是类的名称,它可能不会那么混乱。 @Bill Michell,现在我们进入了发布礼仪的语义。我认为他应该让 Iterator 成为一个指向 Iterator 的 javadoc 的链接,或者至少将它放在内联代码注释 (`) 中。以上是关于迭代 ConcurrentHashMap 值线程安全吗?的主要内容,如果未能解决你的问题,请参考以下文章