Я просто тестирую несколько вещей, чтобы улучшить свое понимание, и написал следующий код для создания 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, я, например, получаю исключение. Есть идеи, почему?
К вашему сведению, очень легко создать CME без многопоточности.
@StephenC В документации упоминается многопоточность. Он также указан в описании тега concurrentmodification, чего бы это ни стоило.
«Есть идеи, почему?» - В случае ArrayList
вызов contains
повторяет коллекцию. В случае HashSet
contains
выполняет поиск по хэш-таблице, который не требует Iterator
. Прочтите javadocs (см. выше), чтобы понять, почему это здесь важно.
Вы изменяете HashSet только дважды, потому что значение уже есть в нем после первого добавления. Обновление вашего кода, чтобы каждый раз добавлять новое значение, например. Привет0, Привет1, ...
Наконец, стоит отметить, что код вашего примера не является потокобезопасным ни в случаях ArrayList
, ни в HashSet
. Это «работает» только потому, что вам повезло (частично) благодаря конкретным операциям, которые вы выполняете. Вам следует либо использовать параллельный класс коллекции или оболочку, либо синхронизировать каждую операцию с коллекцией.
ОП пытается написать код, который генерирует исключение, т. е. пытается сделать код, который НЕ является потокобезопасным.
БертФ - вы неправильно поняли. 1) CCME в первую очередь не касается потокобезопасности. 2) Исключениями (или неправильным поведением), которые могут возникнуть из-за отсутствия синхронизации, могут быть такие вещи, как NPE и AIOOBE. Или contains
звонит и дает неправильный ответ. Или бесконечные циклы со старыми версиями Java IIRC.
(И я не думаю, что намерением ОП было написать в целом непотокобезопасный код. Я думаю, что это было специальное исследование сценария, в котором может возникнуть CCME. Мой комментарий состоял в том, чтобы указать на проблему с потокобезопасностью, отличную от той, которая существует ОП, видимо, пытается понять.)
Кстати... Вызов Executor#execute
может запустить, а может и не запустить вашу задачу в фоновом потоке. Прочтите Javadoc. Если вам определенно нужна задача в фоновом потоке, вызовите методы, специфичные для ExecutorService
, такие как submit
и invokeAll
.
Вы изменяете 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 относится конкретно к этой последовательности событий:
.iterator()
в некоторой коллекции. Обратите внимание, что for (String x : collection)
является синтаксическим сахаром и вызывает .iterator()
..iterator()
), а не через этот итератор, то есть не через вызов .remove()
на итераторе. Например, вы вызываете collection.remove()
, или collection.clear(),
, или collection.add()
..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
. Не пишите код, который ожидает обратного: вам нравится неопределённое поведение — не делайте этого вообще.
Вы взаимодействуете с итератором, который был создан до последнего изменения данной коллекции. Вот и все, и такое поведение может быть гарантировано только в том случае, если вы все делаете это в одном потоке; сделайте это из нескольких потоков, и вступят в силу правила JMM, которые в сочетании с «HashSet и co» намеренно указаны без включения мучительных подробностей о том, как именно они работают, чтобы позволить реализациям JVM предоставлять что-то, что оптимально работает на локальном оборудовании», и в конечном итоге вы получите: «Поведение вашего Java-приложения произвольно и не указано».
[1] Некоторые подтипы, такие как CopyOnWriteList
, имеют документацию, в которой явно описывается, что происходит, когда вы это делаете, и для таких реализаций вы можете положиться на то, что классы делают то, что, согласно их спецификациям, они должны. Однако сам Set не имеет таких спецификаций, и если тип явно не добавляет такую спецификацию, поведение не гарантируется при взаимодействии с ним из нескольких потоков. В частности, HashSet
не дает никаких гарантий относительно того, как он работает или что он делает, когда вы получаете к нему доступ из нескольких потоков, поэтому, если вы не хотите включать проверку точного исходного кода для конкретной версии JVM и версии, на которой вы ее запускаете, все что угодно могло случиться. Возможно, ваш компьютер взорвался. Это было бы плохо, но JVM не «нарушала бы спецификации».
... или носовые демоны.
Где вы ожидаете исключения и почему?