Является ли итерация значений ConcurrentHashMap потокобезопасной?

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

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

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

5 ответов


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

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

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

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

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

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

В заключение, заявление, как

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

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


вы можете использовать этот класс для тестирования двух потоков доступа и одного мутирующего общего экземпляра 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.IllegalStateExceptions начнет появляться.

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);
  }
}

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


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

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

по поводу этого:

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

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


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

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

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

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

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

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

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