Чи безпечна нитка значень ConcurrentHashMap?


156

У javadoc для ConcurrentHashMap є наступне:

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

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

Відповіді:


193

Що це означає?

Це означає, що кожен ітератор, який ви отримуєте від а ConcurrentHashMap, призначений для використання однієї нитки і не повинен передаватися навколо. Сюди входить синтаксичний цукор, який надається для кожного циклу.

Що станеться, якщо я спробую повторити карту двома нитками одночасно?

Він буде працювати, як очікувалося, якщо кожен з потоків використовує власний ітератор.

Що станеться, якщо я вставляю або виймаю значення з карти під час повторення?

Гарантується, що все не зламається, якщо ви це зробите (це частина того, що ConcurrentHashMapозначає «одночасний» ). Однак немає гарантії, що один потік побачить зміни на карті, які виконує інший потік (без отримання нового ітератора з карти). Ітератор гарантовано відображає стан карти під час її створення. Подальші зміни можуть відображатися в ітераторі, але вони не повинні бути.

На закінчення, заява на кшталт

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

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


Отже, що буде, якщо під час ітерації інший потік видалить з карти об’єкт o10? Чи можу я все-таки побачити o10 в ітерації, навіть якщо вона була видалена? @Waldheinz
Alex

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

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 в Iterator? Оскільки це назва класу, це може бути менш заплутаним.
Білл Мішель

1
@Bill Michell, зараз ми в семантиці розміщення етикету. Я думаю, що він повинен був перетворити Iterator на посилання назад до javadoc для Iterator або, принаймні, розмістити його всередині вбудованих анотацій коду (`).
Тім Бендер

10

Це може дати вам гарне уявлення

ConcurrentHashMap досягає вищої конкурентоспроможності, трохи послабивши обіцянки, які він дає абонентам. Операція пошуку поверне значення, вставлене останньою завершеною операцією вставки, а також може повернути додане значення операцією вставки, яка одночасно виконується (але ні в якому разі не поверне результат дурниць). Ітератори, повернені ConcurrentHashMap.iterator (), повертають кожен елемент максимум один раз і ніколи не кидають ConcurrentModificationException, але можуть або не можуть відображати вставки або видалення, що відбулися з моменту побудови ітератора. Не потрібне (або навіть можливе) блокування по всій таблиці, щоб забезпечити безпеку ниток при ітерації колекції. ConcurrentHashMap може використовуватися в якості заміни для синхронізованої карти або Hashtable у будь-якій програмі, яка не покладається на можливість блокування всієї таблиці для запобігання оновлень.

З цього приводу:

Однак ітератори призначені для використання лише однією ниткою за один раз.

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


4

Що це означає?

Це означає, що не слід намагатися використовувати один ітератор у двох потоках. Якщо у вас є два потоки, які потрібно перебирати над ключами, значеннями або записами, то кожен з них повинен створити і використовувати свої власні ітератори.

Що станеться, якщо я спробую повторити карту двома нитками одночасно?

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

Але якщо в двох потоках використовувались різні ітератори, вам слід добре.

Що станеться, якщо я вставляю або виймаю значення з карти під час повторення?

Це окреме питання, але розділ Javadoc, який ви цитували, адекватно відповідає на нього. В основному, ітератори є безпечними для потоків, але не визначено, чи ви побачите ефекти будь-яких одночасних вставок, оновлень або видалень, відображених у послідовності повернених ітератором об'єктів. На практиці це, мабуть, залежить від того, де на карті відбуваються оновлення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.