Эффекты памяти синхронизации в Java - PullRequest
39 голосов
/ 05 декабря 2009

JSR-133 FAQ говорит:

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

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

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

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

Ответы [ 7 ]

41 голосов
/ 08 декабря 2009

Короткий ответ таков: JSR-133 заходит слишком далеко в своем объяснении . Это не серьезная проблема, потому что JSR-133 является ненормативным документом, который не является частью языка или стандартов JVM. Скорее, это всего лишь документ, который объясняет одну возможную стратегию, которой достаточно для реализации модели памяти, но в общем случае не требуется . Кроме того, комментарий о «очистке кеша» в основном совершенно неуместен, поскольку практически нулевые архитектуры реализуют модель памяти Java, выполняя любой тип «очистки кеша» (а многие архитектуры даже не имеют таких инструкций).

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

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

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

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

10 голосов
/ 23 февраля 2012

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

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

7 голосов
/ 05 декабря 2009

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

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

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

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

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

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

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

4 голосов
/ 05 декабря 2009

мы разработчики Java, мы знаем только виртуальные машины, а не реальные машины!

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

скажем, поток A работает на процессоре A с кэшем A, поток B работает на процессоре B с кэшем B,

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

  2. поток B назначает новое значение для 'y'. В этом случае VM не нужно обновлять основную память; что касается потока B, он может быть для чтения / записи на локальном изображении 'y'; может быть, 'y' - это не что иное, как регистр процессора.

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

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

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

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

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


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

3 голосов
/ 28 февраля 2013

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

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

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

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

1 голос
/ 19 августа 2010

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

0 голосов
/ 05 декабря 2009

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...