167

在用于ConcurrentHashMap的javadoc 中如下:

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

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

4

5 回答 5

207

这是什么意思?

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

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

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

如果我在迭代时从映射中放置或删除一个值会发生什么?

如果您这样做,可以保证事情不会中断(这是“并发”ConcurrentHashMap含义的一部分)。但是,不能保证一个线程会看到另一个线程执行的对映射的更改(没有从映射中获取新的迭代器)。迭代器保证在创建地图时反映地图的状态。进一步的变化可能会反映在迭代器中,但不一定非得如此。

总之,像这样的声明

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

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

于 2010-09-22T11:17:50.580 回答
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>>在访问器和修改器线程之间共享相同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);
  }
}
于 2010-09-22T11:18:04.713 回答
12

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

于 2010-09-22T11:14:22.433 回答
9

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

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

关于这一点:

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

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

于 2010-09-22T11:11:30.973 回答
5

这是什么意思?

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

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

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

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

如果我在迭代时从映射中放置或删除一个值会发生什么?

如果两个线程使用相同的迭代器:见上文。您可能会感到困惑并且可能是非线程安全的行为。

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

于 2010-09-22T12:05:21.287 回答