HashMap и видимость

HashMap javadoc утверждает:

если карта структурно модифицирована в любое время после создания итератора, любым способом, кроме метода собственного удаления итератора, итератор будет вызывать исключение ConcurrentModificationException.

Я построил образец кода, который, основываясь на спецификации, должен сбой почти сразу и бросить ConcurrentModificationException;

  • Это происходит не сразу, как ожидалось, с Java 7
  • но он (кажется) всегда работает с Java 6 (т.е. не бросает обещанное исключение).

Примечание: иногда это не прерывается с Java 7 (скажем, 1 раз из 20). Я полагаю, что это связано с планированием потоков (т.е. 2 runnables не чередуются).

Я что-то упустил? Почему версия работает с Java 6, не выдает исключение ConcurrentModificationException?

В сущности, есть 2 Runnable задачи, выполняющиеся параллельно (обратный отсчет используется, чтобы заставить их запускаться примерно в одно и то же время):

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

Затем основной поток проверяет, сколько ключей было добавлено в массив.

типичный вывод Java 7 (итерация прерывается немедленно):

java.util.ConcurrentModificationException
MAX я = 0

типичный вывод Java 6 (вся итерация проходит, а массив содержит все добавленные ключи):

MAX я = 99

Используемый код:

public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        final int SIZE = 100;
        final Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        map.put(1, 1);
        map.put(2, 2);
        map.put(3, 3);
        final int[] list = new int[SIZE];
        final CountDownLatch start = new CountDownLatch(1);
        Runnable put = new Runnable() {
            @Override
            public void run() {
                try {
                    start.await();
                    for (int i = 4; i < SIZE; i++) {
                        map.put(i, i);
                    }
                } catch (Exception ex) {
                }
            }
        };

        Runnable iterate = new Runnable() {
            @Override
            public void run() {
                try {
                    start.await();
                    int i = 0;
                    for (Map.Entry<Integer, Integer> e : map.entrySet()) {
                        list[i++] = e.getKey();
                        Thread.sleep(1);
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        };
        ExecutorService e = Executors.newFixedThreadPool(2);
        e.submit(put);
        e.submit(iterate);
        e.shutdown();

        start.countDown();
        Thread.sleep(100);
        for (int i = 0; i < SIZE; i++) {
            if (list[i] == 0) {
                System.out.println("MAX i = " + i);
                break;
            }
        }
    }
}

Примечание: использование JDK 7u11 и JDK 6u38 (версия 64 бит) на машине x86.

Ответ 1

Если мы рассмотрим источники HashMap и сравним их между Java 6 и Java 7, мы увидим такую ​​интересную разницу:

transient volatile int modCount; в Java6 и только transient int modCount; в Java7.

Я уверен, что из-за этого причиной является другое поведение упомянутого кода:

        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();

UPD: Мне кажется, что это известная ошибка Java 6/7: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6625725 который был исправлен в последнем Java7.

UPD-2: Г-н @Ренджит сказал, что он просто протестировал и не нашел никакой разницы в поведении реализации HashMaps. Но я тоже испытал это.

Мой тест:

1) Я создал класс HashMap2, который является абсолютно копией HashMap из Java 6.

Важно отметить, что нам нужно ввести два новых поля:

transient volatile Set<K>        keySet = null;

и

transient volatile Collection<V> values = null;

2) Затем я использовал этот HashMap2 в тесте на этот вопрос и запустил его в Java 7

Результат: работает как такой тест под Java 6, т.е. нет ConcurentModificationException.

Все это доказывает мою гипотезу. Q.E.D.

Ответ 2

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

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

Ответ 3

Моя теория заключается в том, что на обоих Java 6 & 7 создание итератора в потоке чтения занимает больше времени, чем старение 100 записей в потоке записи, главным образом потому, что новые классы должны быть загружены и инициализированы (а именно EntrySet, AbstractSet, AbstractCollection, Set, EntryIterator, HashIterator, Iterator)

Итак, поток писателя завершился к тому времени, когда эта строка выполняется на поток читателя

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry

В Java 6, поскольку modCount - volatile, итератор видит последний modCount and size, поэтому остальная итерация идет гладко.

В Java 7 modCount нестабилен, итератор, вероятно, видит stale modCount=3, size=3. После sleep(1) итератор видит обновленный modCount и сбой немедленно.

Некоторые недостатки этой теории:

  • теория должна предсказать MAX i=1 на java 7
  • до того, как выполняется main(), HashMap, вероятно, был повторен другим кодом, поэтому упомянутые классы, вероятно, уже были загружены.
  • поток читателя, видящий устаревший modCount, возможен, но вряд ли, так как он сначала считывает переменную в этом потоке; там нет предварительно кэшированного значения.

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