Итерация значений ConcurrentHashMap потокобезопасна? - PullRequest
134 голосов
/ 22 сентября 2010

В javadoc для ConcurrentHashMap имеет следующий вид:

Операции получения (включая получение), как правило, не блокируются, поэтому могут перекрываться с операциями обновления (включая операции добавления и удаления).Извлечения отражают результаты самых последних завершенных операций обновления, проводимых с момента их появления.Для агрегатных операций, таких как putAll и clear, одновременный поиск может отражать вставку или удаление только некоторых записей.Аналогично, Итераторы и Перечисления возвращают элементы, отражающие состояние хеш-таблицы в некоторой точке во время или после создания итератора / перечисления.Они не генерируют ConcurrentModificationException. Однако итераторы предназначены для использования только одним потоком одновременно.

Что это значит?Что произойдет, если я попытаюсь повторить карту с двумя потоками одновременно?Что произойдет, если я добавлю или уберу значение с карты во время итерации?

Ответы [ 5 ]

170 голосов
/ 22 сентября 2010

Что это значит?

Это означает, что каждый итератор, полученный из ConcurrentHashMap, предназначен для использования одним потоком и не должен передаваться.Это включает синтаксический сахар, который предоставляет цикл for-each.

Что произойдет, если я попытаюсь выполнить итерацию карты с двумя потоками одновременно?

Это будетработать как положено, если каждый из потоков использует свой собственный итератор.

Что произойдет, если я добавлю или уберу значение с карты во время итерации?

Гарантируется, что если вы это сделаете, все не сломается (это частьчто означает «одновременный» в ConcurrentHashMap).Однако нет гарантии, что один поток увидит изменения в карте, которые выполняет другой поток (без получения нового итератора из карты).Итератор гарантированно отображает состояние карты на момент ее создания.Дальнейшие изменения могут быть отражены в итераторе, но они не обязательно должны быть.

В заключение, утверждение типа

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

будет хорошо (или, по крайней мере, безопасно) почти каждый разты видишь это.

18 голосов
/ 22 сентября 2010

Вы можете использовать этот класс для проверки двух обращающихся потоков и одного изменения общего экземпляра 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);
  }
}
10 голосов
/ 22 сентября 2010

Это означает, что вы не должны совместно использовать объект итератора между несколькими потоками. Хорошо создать несколько итераторов и использовать их одновременно в разных потоках.

8 голосов
/ 22 сентября 2010

Это может дать вам хорошее представление

ConcurrentHashMap достигает более высокого параллелизма, слегка ослабляя обещания, которые он дает вызывающим.Операция поиска вернет значение, вставленное самой последней завершенной операцией вставки, а также может вернуть значение, добавленное операцией вставки, которая выполняется одновременно (но ни в коем случае она не вернет бессмысленный результат). Итераторы, возвращаемые ConcurrentHashMap.iterator (), будут возвращать каждый элемент максимум один раз и никогда не вызовут исключение ConcurrentModificationException, но могут отражать или не отражать вставки или удаления, которые произошли с момента создания итератора .Никакая блокировка всей таблицы не требуется (или даже возможна) для обеспечения безопасности потока при итерации коллекции.ConcurrentHashMap может использоваться в качестве замены для synchronizedMap или Hashtable в любом приложении, которое не использует возможность блокировки всей таблицы для предотвращения обновлений.

В связи с этим:

Однако итераторы предназначены для использования только одним потоком за один раз.

Это означает, что использование итераторов, созданных ConcurrentHashMap в двух потоках, безопасно, это может привести к неожиданному результату в приложении.

4 голосов
/ 22 сентября 2010

Что это значит?

Это означает, что вы не должны пытаться использовать один и тот же итератор в двух потоках.Если у вас есть два потока, которым нужно перебирать ключи, значения или записи, то каждый из них должен создавать и использовать свои собственные итераторы.

Что произойдет, если я попытаюсь перебрать карту с двумя потоками вв то же время?

Не совсем понятно, что произойдет, если вы нарушите это правило.Вы можете просто получить запутанное поведение, как если бы (например) два потока пытались читать со стандартного ввода без синхронизации.Вы также можете получить не поточнобезопасное поведение.

Но если два потока используют разные итераторы, у вас все будет в порядке.

Что произойдет, если я добавлю или удалим значениекарта во время итерации?

Это отдельная проблема, но приведенный вами раздел javadoc адекватно отвечает на него.По сути, итераторы являются поточно-ориентированными, но не определено , увидите ли вы влияние любых одновременных вставок, обновлений или удалений, отраженных в последовательности объектов, возвращаемых итератором.На практике это, вероятно, зависит от того, где на карте происходят обновления.

...