迭代 ConcurrentHashMap 值是否线程安全?

在 javadoc 中,ConcurrentHashMap的代码如下:

检索操作(包括 get)通常不会阻塞,因此可能与更新操作(包括 put 和 delete)重叠。检索反映了最近完成的更新操作在开始时的结果。对于 putAll 和 clear 之类的聚合操作,并发检索可能只反映某些条目的插入或删除。类似地,迭代器和枚举返回元素,这些元素反映在迭代器/枚举创建之时或之后哈希表的某个时刻的状态。它们不引发 ConcurrentModficationException。但是,迭代器一次只能由一个线程使用。

这是什么意思?如果我尝试同时用两个线程迭代映射会发生什么?如果在迭代映射时从映射中放入或移除一个值,会发生什么情况?

114689 次浏览

这个 可能会给你一个很好的洞察力

ConcurrentHashMap 通过稍微放松对调用者的承诺来实现更高的并发性。检索操作将返回最近完成的插入操作插入的值,还可以返回正在进行的插入操作添加的值(但在任何情况下都不会返回无意义的结果)。由 ConcurrentHashMap.iterator ()返回的迭代器最多返回每个元素一次,并且永远不会抛出 ConcurrentModficationException,但是可能会也可能不会反映自迭代器构造以来发生的插入或删除操作.在迭代集合时,不需要(甚至不可能)表范围的锁来提供线程安全性。在任何不依赖锁定整个表以防止更新的应用程序中,ConcurrentHashMap 都可以用来替代 synizedMap 或 Hashtable。

关于这一点:

但是,迭代器一次只能由一个线程使用。

这意味着,尽管在两个线程中使用 ConcurrentHashMap 产生的迭代器是安全的,但它可能会在应用程序中导致意外的结果。

这意味着您不应该在多个线程之间共享迭代器对象。创建多个迭代器并在单独的线程中并发使用它们是可以的。

这是什么意思?

这意味着从 ConcurrentHashMap获得的每个迭代器都被设计为由单个线程使用,不应该传递。这包括 for-each 循环提供的语法代码。

如果我尝试同时用两个线程迭代映射会发生什么?

如果每个线程都使用自己的迭代器,那么它将按预期工作。

如果在迭代映射时从映射中放入或移除一个值,会发生什么情况?

这样做可以保证事情不会中断(这是 ConcurrentHashMap中“并发”的含义的一部分)。但是,不能保证一个线程会看到另一个线程执行的映射的更改(不从映射中获得新的迭代器)。迭代器保证反映映射在创建时的状态。进一步的更改可能会反映在迭代器中,但它们不一定要反映在迭代器中。

总之,类似于

for (Object o : someConcurrentHashMap.entrySet()) {
// ...
}

几乎每次你看到它都会很好(或者至少是安全的)。

您可以使用这个类来测试两个访问线程和一个变异 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就会开始弹出。

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);
}
}

这是什么意思?

这意味着您不应该尝试在两个线程中使用相同的迭代器。如果有两个线程需要迭代键、值或条目,那么它们应该各自创建并使用自己的迭代器。

如果我尝试同时用两个线程迭代映射会发生什么?

如果你打破了这个规则,会发生什么事情还不完全清楚。您可能会得到令人困惑的行为,就像(例如)两个线程试图从标准输入读取而不进行同步一样。您还可以获得非线程安全的行为。

但是如果两个线程使用不同的迭代器,那么应该没有问题。

如果在迭代映射时从映射中放入或移除一个值,会发生什么情况?

如果两个线程使用相同的迭代器: 参见上文。您很容易产生混淆和可能不是线程安全的行为。

如果线程使用不同的迭代器,那么您引用的 javadoc 部分将充分回答这个问题。基本上,没有定义一个线程/迭代器是否会看到另一个线程/迭代器执行的任何并发插入、更新或删除操作的效果。但是,插入/更新/删除将根据映射的并发属性进行。