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


156

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

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

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

Ответы:


193

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

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

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

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

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

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

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

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

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


Так что же произойдет, если во время итерации другой поток удалил объект o10 с карты? Могу ли я увидеть o10 в итерации, даже если она была удалена? @Waldheinz
Алекс,

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

8
Но у меня все еще есть ConcurrentModificationExceptionитерации ConcurrentHashMap, почему?
Кими Чиу

@KimiChiu, вероятно, вам следует опубликовать новый вопрос, предоставляющий код, вызывающий это исключение, но я сильно сомневаюсь, что это происходит непосредственно из итерации параллельного контейнера. если реализация Java не глючит.
Вальдхайнц

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

Вы уверены, что «Совместное использование одного итератора между потоками доступа может привести к тупику»? В документе говорится, что чтение не заблокировано, и я попробовал вашу программу, и пока не произошло тупиков. Хотя результат итерации будет неправильным.
Тони

12

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


По какой причине вы не использовали I в Итераторе? Поскольку это имя класса, оно может быть менее запутанным.
Билл Мичелл,

1
@ Билл Мичелл, теперь мы находимся в семантике публикации этикета. Я думаю, что он должен был сделать Iterator ссылкой на javadoc для Iterator или, по крайней мере, поместить его в аннотации встроенного кода (`).
Тим Бендер

10

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

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

В соответствии с этим:

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

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


4

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

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

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

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

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.