Итерирует ли ConcurrentHashMap значение потокобезопасности?

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

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

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

Ответ 1

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

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

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

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

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

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

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

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

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

Ответ 2

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

Ответ 3

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

Ответ 4

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

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

Относительно этого:

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

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

Ответ 5

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

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

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

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

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

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

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