Эффекты синхронизации синхронизации в Java

Часто задаваемые вопросы JSR-133:

Но синхронизация больше чем взаимное исключение. синхронизация гарантирует, что память записывается потоком до или во время синхронизированного блока становятся видимыми в предсказуемом образом к другим потокам, которые синхронизироваться на одном мониторе. После мы выходим из синхронизированного блока, мы отпустите монитор, который имеет эффект сброса кеша в основной памяти, так что записи, сделанные этим нить может быть видна другим потоки. Прежде чем мы сможем ввести синхронизированного блока, мы получаем монитора, что аннулирование кеша локального процессора так что переменные будут перезагружены из основной памяти. Тогда мы сможем видеть все сделанные записи по предыдущему выпуску.

Я также помню, как читал, что на современных Sun VM неуправляемые синхронизации дешевы. Я немного смущен этим утверждением. Рассмотрим код типа:

class Foo {
    int x = 1;
    int y = 1;
    ..
    synchronized (aLock) {
        x = x + 1;
    }
}

Обновления x нуждаются в синхронизации, но получает ли блокировка значение y также из кеша? Я не могу представить, чтобы это было так, потому что, если бы это было так, методы, такие как блокировка полосы, могут не помочь. В качестве альтернативы, JVM надежно анализирует код, чтобы гарантировать, что y не изменяется в другом синхронизированном блоке с использованием той же блокировки и, следовательно, не выгружает значение y в кеше при входе в синхронизированный блок?

Ответ 1

Короткий ответ: JSR-133 заходит слишком далеко в своем объяснении. Это не является серьезной проблемой, поскольку JSR-133 является ненормативным документом, который не является частью стандартов языка или JVM.

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

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

На практике гарантии, предлагаемые моделью памяти, намного слабее, чем полный флеш - каждая атомная, concurrency -связанная или блокирующая операция очищает весь кеш, будет чрезмерно дорогим - и это практически никогда не выполняется на практике, Вместо этого используются специальные операции с атомарным ЦП, иногда в сочетании с инструкциями барьер памяти, которые помогают обеспечить видимость и упорядочение памяти. Таким образом, кажущаяся несогласованность между дешевой бесконтактной синхронизацией и "полной очисткой кеша" устраняется, если заметить, что первое верно, а второе - нет - полная флеш требуется модели памяти Java (и на практике не происходит флеш).

Если формальная модель памяти слишком тяжела для переваривания (вы не были бы в одиночку), вы также можете погрузиться глубже в эту тему, взглянув на Doug Lea cookbook, которая на самом деле связана в FAQ JSR-133, но возникает проблема с конкретной аппаратной точки зрения, поскольку она предназначена для авторов компиляторов. Там они говорят о том, какие барьеры необходимы для конкретных операций, включая синхронизацию, - и обсуждаемые там барьеры могут быть легко сопоставлены с реальным оборудованием. Большая часть фактического картирования обсуждается прямо в кулинарной книге.

Ответ 2

Обновления x нуждаются в синхронизации, но происходит ли приобретение блокировки очистите значение y также из кэш? Я не могу представить, чтобы быть случай, потому что, если бы это было так, методы, такие как блокировка чередования, могут не помогите.

Я не уверен, но я думаю, что ответ может быть "да". Рассмотрим это:

class Foo {
    int x = 1;
    int y = 1;
    ..
    void bar() {
        synchronized (aLock) {
            x = x + 1;
        }
        y = y + 1;
    }
}

Теперь этот код небезопасен, в зависимости от того, что происходит в остальной части программы. Тем не менее, я думаю, что модель памяти означает, что значение y, видимое bar, не должно быть старше "реального" значения на момент приобретения блокировки. Это означало бы, что кэш должен быть недействительным для y, а также x.

Также JVM может надежно анализировать кода, чтобы гарантировать, что y не изменяется в другом синхронизированном блоке с использованием тот же замок?

Если блокировка this, этот анализ выглядит так, как это было бы возможно в качестве глобальной оптимизации после того, как все классы были предварительно загружены. (Я не говорю, что это будет легко или стоит...)

В более общих случаях проблема доказательства того, что данный замок используется только когда-либо в связи с данным "владеющим" экземпляром, вероятно, трудноразрешима.

Ответ 3

BeeOnRope прав, текст, который вы цитируете, более подробно описывает типичные детали реализации, чем то, что действительно гарантирует модель памяти Java. На практике часто вы можете видеть, что y фактически очищается от кэшей CPU при синхронизации на x (также, если x в вашем примере является изменчивой переменной, и в этом случае явная синхронизация не требуется для запуска эффекта). Это связано с тем, что на большинстве процессоров (обратите внимание, что это аппаратный эффект, а не то, что описывает JMM), кеш работает с единицами, называемыми линиями кэша, которые обычно длиннее машинного слова (например, шириной 64 байта). Поскольку в кэше могут быть загружены или недействительны только полные строки, есть хорошие шансы, что x и y попадут в одну строку и что промывка одной из них также будет сбрасывать другую.

Можно написать контрольный показатель, который показывает этот эффект. Создайте класс с двумя полными переменными int-полями и пусть два потока выполняют некоторые операции (например, увеличиваются в длинном цикле), один на одном из полей и один на другом. Время операции. Затем вставьте 16 полей int между двумя исходными полями и повторите тест (16 * 4 = 64). Обратите внимание, что массив является просто ссылкой, поэтому массив из 16 элементов не будет делать трюк. Вы можете увидеть значительное улучшение производительности, потому что операции в одном поле больше не будут влиять на другое. Будет ли это работать для вас, будет зависеть от реализации JVM и архитектуры процессора. Я видел это на практике на Sun JVM и типичном ноутбуке x64, разница в производительности была в несколько раз.

Ответ 4

Мы разработчики java, мы знаем только виртуальные машины, а не настоящие машины!

Позвольте мне теоретизировать, что происходит, но я должен сказать, что не знаю, о чем говорю.

говорят, что поток A работает на CPU A с кешем A, поток B работает на CPU B с кешем B,

  • поток A читает y; CPU A извлекает y из основной памяти и сохраняет значение в кеше A.

  • поток B присваивает новое значение 'y'. На этой точке VM не нужно обновлять основную память; что касается потока B, это может быть чтение/запись на локальном изображении 'y'; возможно, "y" - не что иное, как регистр cpu.

  • поток B выходит из блока синхронизации и освобождает монитор. (когда и где он вошел в блок, не имеет значения). поток B обновил довольно некоторые переменные до этого момента, включая "y". Все эти обновления должны быть записаны в основную память.

  • CPU B записывает новое значение y, чтобы поместить 'y' в основную память. (Я полагаю, что) почти МГНОВЕННО, информация "main y обновляется" подключена к кэшу A, а кеш - аннулировать свою собственную копию y. Это должно быть действительно FAST на аппаратном обеспечении.

  • поток A получает монитор и входит в блок синхронизации - в этот момент ему не нужно ничего делать в отношении кеша A. 'y' уже перешел из кеша A. когда поток A снова читает y, он новый из основной памяти с новым значением, назначенным буквой B.

рассмотрим другую переменную z, которая также была кеширована A на этапе (1), но не обновлена ​​нитью B на этапе (2). он может выжить в кеше A до самого шага (5). доступ к "z" не замедляется из-за синхронизации.

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


дополнение к шагу (5): поток A может иметь свой собственный кеш, который даже быстрее, чем кэш A - он может использовать регистр для переменной y. это не будет аннулировано с помощью шага (4), поэтому на шаге (5) поток A должен стереть свой собственный кеш при входе синхронизации. это не огромный штраф, хотя.

Ответ 5

вы можете проверить документацию jdk6.0 http://java.sun.com/javase/6/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility

Свойства согласованности памяти В главе 17 Спецификации языка Java определяется отношение "происшествия до операции" в операциях памяти, таких как чтение и запись общих переменных. Результаты записи одним потоком гарантированно будут видны для чтения другим потоком только в том случае, если операция записи происходит до операции чтения. Синхронизированные и изменчивые конструкции, а также методы Thread.start() и Thread.join() могут формироваться в отношениях-до отношений. В частности:

  • Каждое действие в потоке происходит - перед каждым действием в этом потоке, которое приходит позже в порядке выполнения программы.
  • Разблокировка (синхронизированный блок или выход метода) монитора происходит до каждой последующей блокировки (синхронизированный блок или ввод метода) того же монитора. И поскольку отношение "бывает раньше" транзитивно, все действия потока до разблокировки происходят до всех действий, следующих за любой блокировкой потока, которые контролируют.
  • Записывается в нестабильное поле - перед каждым последующим чтением этого же поля. Записи и чтения изменчивых полей имеют аналогичные эффекты согласованности с памятью как входные и выходящие мониторы, но не влекут за собой блокировку взаимного исключения.
  • Запускается вызов для начала в потоке - перед любым действием в запущенном потоке.
  • Все действия в потоке происходят до того, как любой другой поток успешно возвращается из соединения в этом потоке

Итак, как указано в выделенной точке выше: все изменения, которые происходят до разблокировки на мониторе, видны для всех этих потоков (и там есть собственный блок синхронизации), которые блокируют тот же monitor.This в соответствии с Java происходит-до семантики. Поэтому все изменения, сделанные в y, также будут удалены в основную память, когда какой-либо другой поток получит монитор на "aLock".

Ответ 6

синхронизировать гарантии, что только один поток может вводить блок кода. Но это не гарантирует, что изменения переменных, выполненные в синхронизированном разделе, будут видны другим потокам. Только потоки, входящие в синхронизированный блок, гарантированно будут видеть изменения. Эффекты синхронизации синхронизации в Java можно сравнить с проблемой Double-Checked Locking относительно С++ и Java Double-Checked Locking широко цитируется и используется как эффективный метод для реализации ленивой инициализации в многопоточной среде. К сожалению, он не будет надежно работать независимым от платформы способом, реализованным в Java, без дополнительной синхронизации. При реализации на других языках, таких как С++, это зависит от модели памяти процессора, переупорядочений, выполняемых компилятором, и взаимодействия между компилятором и библиотекой синхронизации. Поскольку ни один из них не указан на языке, таком как С++, мало что можно сказать о ситуациях, в которых он будет работать. Явные барьеры памяти могут использоваться, чтобы заставить его работать на С++, но эти барьеры недоступны в Java.

Ответ 7

Так как y выходит за рамки синхронизированного метода, нет никаких гарантий того, что изменения в нем видны в других потоках. Если вы хотите гарантировать, что изменения в y будут одинаковыми для всех потоков, тогда все потоки должны использовать синхронизацию при чтении/записи y.

Если некоторые потоки меняют y синхронно, а другие - нет, вы получите неожиданное поведение. Все изменчивое состояние, разделяемое между потоками, должно быть синхронизировано, чтобы получить какие-либо гарантии при просмотре изменений между потоками. Весь доступ ко всем потокам к общему изменяемому состоянию (переменным) должен быть синхронизирован.

И да, JVM гарантирует, что пока блокировка не удерживается, ни один другой поток не может войти в область кода, защищенную той же блокировкой.