ConcurrentHashMap的值迭代是否线程安全?

171
在Java文档中,ConcurrentHashMap的说明如下:
检索(包括get)通常不会被阻塞,因此可能与更新操作(包括put和remove)重叠。检索反映最近完成的更新操作的结果,在其开始时保持一致性。对于像putAll和clear这样的聚合操作,同时进行的检索可能只反映了某些条目的插入或删除。同样,迭代器和枚举器将返回反映哈希表状态的元素,该状态为在创建迭代器/枚举器时或之后的某个时间点。它们不会抛出ConcurrentModificationException异常。然而,迭代器只设计用于一次只能由一个线程使用。
这是什么意思?如果我尝试同时使用两个线程迭代Map会发生什么?如果在迭代Map时放置或删除值会发生什么?
5个回答

215
这意味着每个从ConcurrentHashMap中获得的迭代器都设计为仅由单个线程使用,不应该传递。这包括for-each循环提供的语法糖。
如果同时使用两个线程,每个线程使用自己的迭代器,那么遍历该映射将按预期工作。
在迭代映射时,如果向其中添加或删除值,则保证不会发生错误(这是ConcurrentHashMap中“并发”的一部分)。但是,并不能保证一个线程将看到另一个线程对映射所做的更改(除非从映射中获取新迭代器)。迭代器保证反映其创建时的映射状态。进一步的更改可能会在迭代器中反映出来,但是并不一定。
总之,像以下这样的语句:
for (Object o : someConcurrentHashMap.entrySet()) {
    // ...
}

在几乎所有情况下,这种做法都会是良好的(或至少是安全的)。


在迭代过程中,如果另一个线程从映射中删除了对象o10,会发生什么?即使已经被删除,我仍然可以在迭代中看到o10吗?@Waldheinz - Alex
如上所述,如果现有的迭代器将反映地图的后续更改,这确实没有明确说明。因此,我不知道,根据规范,没有人知道(除非查看代码,并且随着运行时每次更新可能会发生变化)。因此,您不能依赖它。 - Waldheinz
11
在使用迭代器遍历 ConcurrentHashMap 时,你仍然遇到了 ConcurrentModificationException 异常,为什么? - Kimi Chiu
@KimiChiu 你可能需要发布一个新问题,提供触发该异常的代码,但我非常怀疑它直接源于迭代并发容器。除非Java实现有缺陷。 - Waldheinz

18

您可以使用这个类来测试两个访问线程和一个改变共享实例ConcurrentHashMap的线程:

您可以使用此类测试两个访问线程和一个更改共享实例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);
  }
}

你确定“在访问器线程之间共享相同的迭代器可能会导致死锁”吗?文档中说读取不会被阻塞,我也尝试了你的程序,但还没有发生死锁。虽然迭代结果将是错误的。 - Tony

12

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


你为什么没有将 Iterator 中的 I 大写呢?因为它是类的名称,大写可能会更清晰明了。 - Bill Michell
1
@Bill Michell,现在我们来谈论一下发布帖子的礼仪语义。我认为他应该将Iterator作为指向Iterator javadoc的链接,或者至少将其放置在内联代码注释(`)中。 - Tim Bender

10

这篇文章或许能帮您深入了解。

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

关于这个:

然而,迭代器只被设计给一个线程使用。

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


5
这意味着您不应该在两个线程中使用相同的迭代器。如果有两个线程需要遍历键、值或条目,则它们应该分别创建并使用自己的迭代器。
如果违反此规则,会发生什么还不太清楚。您可能会得到混乱的行为,就像两个线程在没有同步的情况下尝试从标准输入读取一样。您也可能会得到非线程安全的行为。
但是,如果两个线程使用不同的迭代器,则应该没问题。
如果在迭代时将值放入或从地图中删除,如果两个线程正在使用相同的迭代器,则会出现与上述相同的问题。如果线程使用不同的迭代器,则您引用的javadoc部分已经足够回答了这个问题。基本上,不能确定一个线程/迭代器是否会看到另一个线程/迭代器进行的任何并发插入、更新或删除的影响。但是,插入/更新/删除将根据地图的并发属性执行。

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