Создание ConcurrentModificationExecption с помощью Java HashSet

Я просто тестирую несколько вещей, чтобы улучшить свое понимание, и написал следующий код для создания ConcurrentModificationExecption.

public static void main(String[] args) throws InterruptedException {
    Set<String> set = new HashSet<>();

    Runnable updateList = () -> {
        while (true) {
            set.add("Hello");
        }
    };
    ExecutorService executorService1 = Executors.newSingleThreadExecutor();
    executorService1.execute(updateList);

    Runnable printList = () -> {
        while (true) {
            if (set.contains("Hello")) {
                System.out.println(set);
                set.add("Bye");
            }
        }
    };
    ExecutorService executorService2 = Executors.newSingleThreadExecutor();
    executorService2.execute(printList);

    Thread.sleep(1000);
    executorService1.shutdown();
    executorService2.shutdown();
}

Однако это не создает исключения, как я думал. Когда я заменяю HashSet на ArrayList, я, например, получаю исключение. Есть идеи, почему?

Где вы ожидаете исключения и почему?

shmosel 02.08.2024 03:47

К вашему сведению, очень легко создать CME без многопоточности.

shmosel 02.08.2024 03:47

@StephenC В документации упоминается многопоточность. Он также указан в описании тега concurrentmodification, чего бы это ни стоило.

shmosel 02.08.2024 03:50

«Есть идеи, почему?» - В случае ArrayList вызов contains повторяет коллекцию. В случае HashSetcontains выполняет поиск по хэш-таблице, который не требует Iterator. Прочтите javadocs (см. выше), чтобы понять, почему это здесь важно.

Stephen C 02.08.2024 03:51

Вы изменяете HashSet только дважды, потому что значение уже есть в нем после первого добавления. Обновление вашего кода, чтобы каждый раз добавлять новое значение, например. Привет0, Привет1, ...

Bert F 02.08.2024 04:03

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

Stephen C 02.08.2024 04:05

ОП пытается написать код, который генерирует исключение, т. е. пытается сделать код, который НЕ является потокобезопасным.

Bert F 02.08.2024 04:07

БертФ - вы неправильно поняли. 1) CCME в первую очередь не касается потокобезопасности. 2) Исключениями (или неправильным поведением), которые могут возникнуть из-за отсутствия синхронизации, могут быть такие вещи, как NPE и AIOOBE. Или contains звонит и дает неправильный ответ. Или бесконечные циклы со старыми версиями Java IIRC.

Stephen C 02.08.2024 04:10

(И я не думаю, что намерением ОП было написать в целом непотокобезопасный код. Я думаю, что это было специальное исследование сценария, в котором может возникнуть CCME. Мой комментарий состоял в том, чтобы указать на проблему с потокобезопасностью, отличную от той, которая существует ОП, видимо, пытается понять.)

Stephen C 02.08.2024 04:18

Кстати... Вызов Executor#execute может запустить, а может и не запустить вашу задачу в фоновом потоке. Прочтите Javadoc. Если вам определенно нужна задача в фоновом потоке, вызовите методы, специфичные для ExecutorService, такие как submit и invokeAll.

Basil Bourque 02.08.2024 05:28
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
10
53
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы изменяете HashSet только дважды, потому что значение уже есть в нем после первого добавления. Обновление вашего кода, чтобы каждый раз добавлять новое значение, например. Привет0, Привет1, ...

public static void main(String[] args) throws InterruptedException {
    Set<String> set = new HashSet<>();

    Runnable updateList = () -> {
        int i = 0;
        while (true) {
            set.add("Hello"+i);
            i++;
        }
    };
    ExecutorService executorService1 = Executors.newSingleThreadExecutor();
    executorService1.execute(updateList);

    Runnable printList = () -> {
        int i = 0;
        while (true) {
            if (set.contains("Hello0")) {
                System.out.println(set);
                set.add("Bye"+i);
                i++;
            }
        }
    };
    ExecutorService executorService2 = Executors.newSingleThreadExecutor();
    executorService2.execute(printList);

    Thread.sleep(1000);
    executorService1.shutdown();
    executorService2.shutdown();
}


$ javac Main3.java && java Main3
Exception in thread "pool-2-thread-1" java.util.ConcurrentModificationException
    at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1605)
    at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1628)
    at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:458)
    at java.base/java.lang.String.valueOf(String.java:4461)
    at java.base/java.io.PrintStream.println(PrintStream.java:1187)
    at Main3.lambda$main$1(Main3.java:23)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
    at java.base/java.lang.Thread.run(Thread.java:1583)
Ответ принят как подходящий

ConcurrentModificationException вообще не имеет ничего общего с потоками.

На самом деле, вам не гарантировано получение CoModEx в одновременных (потоковых) ситуациях, и точка 1. «Параллельность» в ConcurrentModificationException не относится к «параллелизму» в смысле «несколько потоков»!

Этот код создаст CoModEx, поскольку этот фрагмент показывает «параллельный», на который ссылается CoModEx:

Set<String> s = new HashSet<String>();
s.add("Hello"); s.add("World!"); s.add("Foobar");

for (String elem : s) {
  s.remove(elem);
}

Примечательно, что этот код гарантированно выдает CoModEx: он будет делать это каждый раз, на всех JVM, на всех ОС, на всех архитектурах, обязательно так. Если приведенный выше код не создает CoModEx, ваша JVM неисправна.

А ваш фрагмент — CoModEx — это всего лишь одна из миллиона вещей, которые могут произойти.

CoModEx относится конкретно к этой последовательности событий:

  1. Вы создаете итератор, вызывая .iterator() в некоторой коллекции. Обратите внимание, что for (String x : collection) является синтаксическим сахаром и вызывает .iterator().
  2. Вы изменяете базовую коллекцию (ту, которую вы вызвали .iterator()), а не через этот итератор, то есть не через вызов .remove() на итераторе. Например, вы вызываете collection.remove(), или collection.clear(),, или collection.add().
  3. Вы касаетесь итератора; вы вызываете .hasNext() или .next() (что, опять же, for (String x : collection) делает по своей сути).

Эта последовательность событий приводит к CoModEx.

Так что же происходит при столкновении потоков и параллелизма?

Все. Ничего. Магия. Кто знает??

В любое время, когда 1 поток записывает в поле, а другой поток читает из него, поток чтения может законно наблюдать любое значение — он может наблюдать значение до обновления или после — решение остается на усмотрение JVM. Если код записи не имеет установленной связи Happens-Before по сравнению с кодом чтения, а спецификация языка Java точно не перечисляет, какие действия вызывают связь Happens-Before (обычно это включает volatile или synchronized или другие примитивы параллельного выполнения потоков; игры, которые HashSet не играй!) - И каждый раз волен принимать новое решение. Это связано с тем, что JVM написана для нескольких архитектур и предназначена для работы быстрее, чем «ужасно медленно», поэтому ее поведение в таких сложных сценариях должно быть «самым быстрым на локальном оборудовании», а «самое быстрое», следовательно, зависит от того, что оборудование есть.

На практике, когда вы звоните, например. someSet.add(foo), без сомнения, некоторые поля в конечном итоге устанавливаются где-то во время выполнения метода add HashSet. Какие именно из них являются деталями реализации, которые намеренно не указаны и намеренно не определены поведением в соответствии со спецификацией Java. Таким образом, некоторые поля затрагиваются этим, но вы не можете полагаться на какое-либо конкретное определение. Тем не менее, согласно спецификации, любые измененные поля будут «передавать» свои изменения или нет по прихоти JVM, на которой вы это запускаете, в другие потоки. Так что же на самом деле происходит?

Все. Ничего. Магия. Кто знает??

CoModEx может случиться. Или коллекция отвечает ложью (x.contains(y) возвращает false, но x.get(y) возвращает ненулевое значение. Или наоборот. Или x.get(y) зависает навсегда. Или выбрасывает StackOverflowError. Или ConcurrentModificationException. Или, возможно, в зависимости от фазы луны, JVM особенно благосклонно относится к ЕС, и из ваших динамиков звучит «Ода радости».

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

Единственный способ выиграть эту игру — не играть. Не пишите такой код.

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

Так что же означает CoModEx?

Вы взаимодействуете с итератором, который был создан до последнего изменения данной коллекции. Вот и все, и такое поведение может быть гарантировано только в том случае, если вы все делаете это в одном потоке; сделайте это из нескольких потоков, и вступят в силу правила JMM, которые в сочетании с «HashSet и co» намеренно указаны без включения мучительных подробностей о том, как именно они работают, чтобы позволить реализациям JVM предоставлять что-то, что оптимально работает на локальном оборудовании», и в конечном итоге вы получите: «Поведение вашего Java-приложения произвольно и не указано».


[1] Некоторые подтипы, такие как CopyOnWriteList, имеют документацию, в которой явно описывается, что происходит, когда вы это делаете, и для таких реализаций вы можете положиться на то, что классы делают то, что, согласно их спецификациям, они должны. Однако сам Set не имеет таких спецификаций, и если тип явно не добавляет такую ​​спецификацию, поведение не гарантируется при взаимодействии с ним из нескольких потоков. В частности, HashSet не дает никаких гарантий относительно того, как он работает или что он делает, когда вы получаете к нему доступ из нескольких потоков, поэтому, если вы не хотите включать проверку точного исходного кода для конкретной версии JVM и версии, на которой вы ее запускаете, все что угодно могло случиться. Возможно, ваш компьютер взорвался. Это было бы плохо, но JVM не «нарушала бы спецификации».

... или носовые демоны.

Stephen C 02.08.2024 06:30

Другие вопросы по теме