Недавно мы обсуждали мою работу о том, нужно ли нам использовать ConcurrentHashMap или мы можем просто использовать обычный HashMap в нашей многопоточной среде. У HashMaps два аргумента: он быстрее, чем ConcurrentHashMap, поэтому мы должны использовать его, если возможно. И ConcurrentModificationException, по-видимому, появляется только тогда, когда вы перебираете карту по мере ее изменения, поэтому "если мы только PUT и GET из карты, в чем проблема с обычным HashMap?" были аргументы.

Я думал, что одновременные действия PUT или одновременные PUT и READ могут привести к исключениям, поэтому я собрал тест, чтобы показать это. Тест прост; создать 10 потоков, каждый из которых записывает в карту одни и те же 1000 пар ключ-значение снова и снова в течение 5 секунд, а затем распечатать получившуюся карту.

На самом деле результаты были довольно запутанными:

Length:1299
Errors recorded: 0

Я думал, что каждая пара "ключ-значение" уникальна в HashMap, но, просматривая карту, я могу найти несколько идентичных пар "ключ-значение". Я ожидал какого-то исключения или поврежденных ключей или значений, но не ожидал этого. Как это происходит?

Вот код, который я использовал для справки:

public class ConcurrentErrorTest
{
    static final long runtime = 5000;
    static final AtomicInteger errCount = new AtomicInteger();
    static final int count = 10;

    public static void main(String[] args) throws InterruptedException
    {
        List<Thread> threads = new LinkedList<>();
        final Map<String, Integer> map = getMap();

        for (int i = 0; i < count; i++)
        {
            Thread t = getThread(map);
            threads.add(t);
            t.start();
        }

        for (int i = 0; i < count; i++)
        {
            threads.get(i).join(runtime + 1000);
        }

        for (String s : map.keySet())
        {
            System.out.println(s + " " + map.get(s));
        }
        System.out.println("Length:" + map.size());
        System.out.println("Errors recorded: " + errCount.get());
    }

    private static Map<String, Integer> getMap()
    {
        Map<String, Integer> map = new HashMap<>();
        return map;
    }

    private static Map<String, Integer> getConcMap()
    {
        Map<String, Integer> map = new ConcurrentHashMap<>();
        return map;
    }

    private static Thread getThread(final Map<String, Integer> map)
    {
        return new Thread(new Runnable() {
            @Override
            public void run()
            {
                long start = System.currentTimeMillis();
                long now = start;
                while (now - start < runtime)
                {
                    try
                    {
                        for (int i = 0; i < 1000; i++)
                            map.put("i=" + i, i);
                        now = System.currentTimeMillis();
                    }
                    catch (Exception e)
                    {
                        System.out.println("P - Error occured: " + e.toString());
                        errCount.incrementAndGet();
                    }
                }
            }
        });
    }
}
6
Gikkman 25 Ноя 2016 в 15:43

2 ответа

Лучший ответ

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

Когда вы вставляете запись на карту, по крайней мере должны произойти две вещи:

  1. Проверьте, существует ли уже ключ.
  2. Если проверка вернула истину, обновите существующую запись, если нет, добавьте новую.

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

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

9
biziclop 25 Ноя 2016 в 13:24

В многопоточной среде вам всегда следует использовать CuncurrentHashMap, если вы собираетесь выполнять какую-либо операцию, кроме get.

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

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

0
tomerpacific 1 Апр 2019 в 09:12