(Я не знаю Java так хорошо, просто основные концепции блокировки и упорядочивания памяти, которые раскрывает Java. Некоторые из них основаны на предположениях о том, как Java работает, поэтому исправления приветствуются.)
Я бы предположил, что JVM может и будет оптимизировать их в регистры, если вы многократно обращаетесь к ним внутри того же блока synchronized
.
т.е. открытие {
и закрытие }
- это барьеры памяти ( получение и снятие блокировки), но внутри этого блока применяются обычные правила.
Обычные правила для переменных, отличных от volatile
, похожи на в C ++: JIT-компилятор может хранить частные копии / временные файлы и выполнять полную оптимизацию. Закрытие }
делает все назначения видимыми перед тем, как пометить блокировку как освобожденную, поэтому любой другой поток, выполняющий тот же синхронизированный блок, увидит эти изменения.
Но если вы читаете / записываете эти переменные снаружи a synchronized(_lock)
, пока выполняется этот synchronized
блок, нет никакой гарантии упорядочивания и только те гарантии атомарности, которые есть у Java. Только volatile
заставит JVM перечитывать переменную при каждом доступе.
большую часть времени потоки находятся в состоянии WAITING, и пропускная способность также очень низкая. Отсюда необходимость оптимизации.
То, о чем вы беспокоитесь, на самом деле не объясняет этого. Неэффективная генерация кода внутри критического раздела может занять несколько больше времени, что может привести к дополнительным конфликтам.
Но не было бы достаточно большого эффекта, чтобы заставить большинство потоков блокироваться в ожидании блокировок (или I / O?) Большую часть времени, по сравнению с тем, что большую часть времени активно работает большинство потоков.
Комментарий @ Kayaman, скорее всего, правильный: это проблема дизайна, слишком много работы выполняется внутри одного большого мьютекса . Я не вижу циклов внутри вашего критического раздела, но, по-видимому, некоторые из тех методов, которые вы вызываете, содержат циклы или являются дорогостоящими, и никакой другой поток не может войти в этот блок synchronized(_lock)
, пока в нем находится один поток.
Теоретическое замедление в худшем случае для сохранения / перезагрузки из памяти (например, компиляция C в антиоптимизированном режиме отладки) по сравнению с сохранением переменной в регистре будет для чего-то вроде while (--shared_var >= 0) {}
, что даст, возможно, 6-кратное замедление на текущее оборудование x86. (Задержка в 1 цикл для dec eax
по сравнению с этой задержкой плюс 5 циклов пересылки хранилища для адресата памяти dec
). Но это только в том случае, если вы зацикливаете общую переменную или иным образом создаете цепочку зависимостей путем ее повторной модификации.
Обратите внимание, что буфер хранилища с перенаправлением хранилища по-прежнему сохраняет его локальным по отношению к ядру ЦП, даже не необходимость фиксации в кеше L1d.
В гораздо более вероятном случае кода, который просто читает переменную несколько раз, антиоптимизированный код, который действительно загружается каждый раз, может очень эффективно воздействовать на все эти нагрузки в кеш L1d. На x86 вы, вероятно, вряд ли заметите разницу, поскольку современные процессоры имеют пропускную способность нагрузки 2 / такт и эффективную обработку инструкций ALU с операндами источника памяти, например, cmp eax, [rdi]
в основном так же эффективны, как cmp eax, edx
.
(ЦП имеют согласованные кеши, поэтому нет необходимости сбрасывать или полностью переходить в DRAM, чтобы гарантировать, что вы «видите» данные из других ядер; компилятор JVM или C должен только убедиться, что загрузка или сохранение действительно происходит в asm , не оптимизированы в регистр. Регистры являются частными для потоков.)
Но, как я уже сказал, нет никаких причин ожидать, что ваша JVM выполняет эту антиоптимизацию внутри блоков synchronized
. Но даже если бы это было так, это могло бы замедлить 25%.