05:06
0
A: Как работает увеличение счетчика в Java без использования блокировок, volatile и synchronized, в двух потоках

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

1) В условии оговаривается, что есть 2 потока Т1 и Т2. Да, если будет третий поток, занятый только чтением неволатильной переменной, может получиться ваш сценарий. 2) JSR-133 представляет только исторический интерес.
@NowhereMan там вопрос не имеет слов "без каких либо блокировок без volatile, без synchronized.", зато там есть start() и есть join() устанавливающие отношение "произошло до" от и к тому самому третьему потоку. А здесь start() и есть join() запрещены.
/me смотрит на заголовок вопроса minimum value of a counter accessed by two threads without synchronization, потом на код в вопросе, в котором нет volatile, synchronized, равно как и Lock, Semaphore, CountDownLatch и т.п., пытается найти в обоих вопросах какое-то упоминание о запрете join, и морщит лоб, пытаясь понять, где же так нагрешил...
@NowhereMan , Там в вопросе есть корректный код (который я сразу не оценил) и вопрос о том что код выдаст, и start() и join(), возможно, дают тот ответ, который там есть. Если же брать два потока в вакууме, то к моменту окончания потока A, он не обязан видеть, ни то, что он сам записал на шаге 11 (т.е. 10), ни то что записал B на шаге 13 (т.е. 2), а может видеть то, что записал B на шаге 2 (т.е. 1). P.S. Весьма похоже на проблему double-locking, в общем, это разные вопросы и они имеют разные ответы.
И оба потока останутся в зависшем состоянии, как Ахиллес, который никода не догонит черепаху?
05:06
Почему нет? Когда выполняется finalize()? Может никогда, так и в этом вопросе. Мы ж не знаем, зачем вопрос задан, может автор ДСЧ хочет соорудить наблюдая из третьего потока без синхронизаций? 😉
"на какой нибудь странной машине как запись по 1 биту" - Java работает только на железе, которое пишет и читает по четыре байта за раз. То есть, у другого потока нет возможности прочитать int в промежуточном состоянии, когда один байт уже переписан, а другой ещё нет.
При всем уважении, на мой взгляд, слишком много допущений, только чтобы доказать какие-то существенные отличия двух вопросов, в частности, предположение о существовании JVM "с атомарными операциями", что в случае пост-инкремента не слишком выгодно
@NowhereMan , там был конкретный вопрос: что выдаст println(), "произошёл после" двух join(), которые "произошли после" операторов записи (любого на выбор) шага 11 и шага 13, последний связан, либо с оператором записи шага 5, либо с любой записью потока A. Минимальная из которых произошла на шаге 6. Итого минимум - 2. Поскольку пошатывая схема дана для синхронной памяти Intel/AMD, там немного больше шагов, чем надо, можно было просто взять нужный, но результат не меняется, поток B после инкремента запишет минимум 2). При предположении, что JVM записывает результат инкремента в один приём.
@NowhereMan, а в этом вопросе нет конкретного println(). К примеру, если в оба потока добавить по println(), то в этом сценарии, один println() потока A может напечатать минимум 1, а println() потока B - минимум 2. Ну, или наоборот, при симметричном сценарии. Вот и вся разница. (Да, если как-нибудь странная JVM будет писать в несколько приёмов, то возможно будет использовать запись 0 из другого несинхронизированного потока).
Снова какие-то допущения, взятые с потолка.. Если в потоки добавлять println, то скорей всего эта операция в классе PrintStream будет синхронизирована, и она сама по себе достаточно тяжелая, чтобы не повлиять на чередование переключения потоков.
Главная разница между двумя вопросами в том, что в английской версии автор приложил хоть какие-то усилия и написал базовый код для моделирования ситуации. Здесь же автор ограничился словесным описанием проблемы и привёл ряд ограничений. Но всё же можно предположить, что автору вопроса нужно знать значение counter именно по окончании обоих потоков, что подразумевает применение как join, так и единственной операции вывода значения. И вряд ли автора вопроса заботит результат данной программы при запуске спец-JVM на какой-нибудь "Сетуни" с троичной логикой.
@NowhereMan, результат println() зависит от его размещения в состоянии гонок - это объективный факт. Так? Так. То что, в том вопросе место println() фиксировали, просто несколько сузило рассмотрение интересных эффектов поведения гонок. Впрочем, всю его многогранность трудно полностью описать. Например, забавно, что если в конце потоков добавить оператор counter = counter; или, там result = counter;, то минимально возможный результат так же уменьшится, причём для всех рассмотренных вариантов, включая вариант синхронизации join(). 😉 Что до автора этого вопроса, будем посмотреть. 😉
P.S. В модели памяти Java не важно сколько времени прошло от последней суммы до println(), во всех случаях println() могут выполнятся много позже окончания суммирования.
P.P.S. "... Если в потоки добавлять println, то скорей всего эта операция в классе PrintStream будет синхронизирована, ... " то что она тяжелая или синхронизированная, это неважно, видимое значение переменной couner данного потока на данном шаге будет скопировано в стек аргументов, а там трава не расти, результаты будут предсказанные (точнее, математически доказанные). 😉 Хотя она и так не прорастёт, ибо без шансов, поскольку там несвязанные с нами объекты синхронизации, нет у них с нами нужных отношений "произошло до". 😉
@NowhereMan , "чередование переключения потоков", да, отмечу, что современные процессоры и современную память (особенно IBM/Sparc/ARM) невозможно описать в терминах "чередования", но только в терминах моделей памяти Java или C/C++.
avp
avp
05:06
Ну, да, минимальное видимое одним из потоков перед своим завершением значение м.б. 1, но, с тем, что после завершения обеих потоков, в переменной (т.е. в памяти) может быть минимум 2, вы согласны?
@avp, во-первых, видимое одним из потоков неопределённо долгое время, в частности, заведомо после окончания всех операций с counter (что вполне себе, на каком-нибудь Power, или даже ARM). Во-вторых, в более менее традиционной памяти, да, минимум 2, при разумных предположения о JIT/JVM, но уже существуют системы NUMA памяти, которые, в традиционном смысле, фактически состоят только из кэш памяти. В-третих, и это, по сути, главное, в модели памяти Java нет содержимого памяти, в ней определятся только правила видимости, и это практическая потребность и направление развития. 😉
avp
avp
@Serge3leo, imho зря вы так пытаетесь разделить кэш и память. На уровне прикладных программ в современных кэш-когерентных системах кэша вы вообще не видите (по крайней мере без таких ухищрений, которые использовал meltdown и т.п.). Когда вы обращаетесь к некоторому адресу система доставляет вам его актуальное содержимое (не важно, было ли оно в момент обращения в DDR (или другом типе RAM) в L1 какого-то из PE в единственном экземпляре или из его копии в одном из ближайших к вам L3 или ... и т.д. и т.п.). Не думаю, что абстракции Java-памяти могут тут что-то изменить
@avp, да, на Intel/AMD, обычно используют более/менее когерентные (синхронные) системы кэш, точнее говоря, раньше использовали, сейчас допускается асинхронность StoreLoad, плюс возможность спекулятивных вычислений. А на IBM/Sparc/ARM ещё с прошлого века используют иные, более масштабируемые, схемы кэш. Ну, а абстракции памяти Java (или C/C++) определяют минимальные гарантии, которые должны обеспечить кэш (или, бывает, распределённая память), ЦП, в части, спекулятивных вычислений (барьеров) и компиляторы.