迭代ConcurrentHashMap值线程安全吗?


156

在javadoc中,ConcurrentHashMap如下:

检索操作(包括get)通常不会阻塞,因此可能与更新操作(包括put和remove)重叠。检索反映了自发生以来最新完成的更新操作的结果。对于诸如putAll和clear的聚合操作,并发检索可能仅反映某些条目的插入或删除。同样,迭代器和枚举返回的元素反映了在创建迭代器/枚举时或此后某个时刻哈希表的状态。他们不抛出ConcurrentModificationException。但是,迭代器被设计为一次只能由一个线程使用。

这是什么意思?如果我尝试同时使用两个线程迭代地图,会发生什么情况?如果在迭代过程中从地图上放置或删除值会怎样?

Answers:


193

这是什么意思?

这意味着您从中获得的每个迭代器ConcurrentHashMap都旨在由单个线程使用,并且不应被传递。这包括for-each循环提供的语法糖。

如果我尝试同时使用两个线程迭代地图,会发生什么情况?

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

如果在迭代过程中从地图上放置或删除值会怎样?

如果这样做,可以确保一切都不会中断(这是“并发” ConcurrentHashMap意味着的一部分)。但是,不能保证一个线程会看到另一线程执行的映射更改(无需从映射中获取新的迭代器)。保证迭代器在创建地图时能够反映其状态。进一步的更改可能会反映在迭代器中,但不一定如此。

总之,类似

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

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


那么,如果在迭代过程中,另一个线程从映射中删除了对象o10,将会发生什么?即使已删除o10,我仍然可以看到它吗?@Waldheinz
Alex

如上所述,实际上并没有指定现有的迭代器是否会反映以后对地图的更改。因此,我不知道,按照规范,没有人会这样做(不查看代码,并且随着运行时的每次更新,情况都会有所变化)。因此,您不能依靠它。
Waldheinz

8
但是我仍然有一段ConcurrentModificationException时间迭代a ConcurrentHashMap,为什么呢?
Kimi Chiu

@KimiChiu您可能应该发布一个新问题,以提供触发该异常的代码,但我高度怀疑它直接源于并发容器的迭代。除非Java实现存在错误。
Waldheinz

18

您可以使用此类来测试两个正在访问的线程,并测试一个正在更改的共享实例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>>在访问者线程和mutator线程之间共享它们,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);
  }
}

您是否确定“在访问器线程之间共享相同的迭代器会导致死锁”?该文件说未阻止读取,并且我尝试了您的程序,但尚未发生死锁。尽管迭代结果将是错误的。
托尼

12

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


您有没有在Iterator中不使用I的原因吗?由于它是类的名称,因此可能会减少混乱。
Bill Michell 2010年

1
@Bill Michell,现在我们处于张贴礼节的语义中。我认为他应该使Iterator成为指向Iterator的javadoc的链接,或者至少将其放置在内联代码注释(`)中。
Tim Bender 2010年

10

可能会给您一个很好的见解

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

关于此:

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

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


4

这是什么意思?

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

如果我尝试同时使用两个线程迭代地图,会发生什么情况?

目前尚不清楚,如果您违反此规则,将会发生什么。您可能会得到混乱的行为,就像(例如)两个线程尝试从标准输入读取而不进行同步一样。您还可以得到非线程安全的行为。

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

如果在迭代过程中从地图上放置或删除值会怎样?

那是一个单独的问题,但是您引用的javadoc部分足以解决这个问题。基本上,迭代器是线程安全的,但是并没有定义是否会在迭代器返回的对象序列中看到任何并发插入,更新或删除的影响。实际上,这可能取决于更新在地图中的何处发生。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.