2009-12-04 2 views
32

JSR-133 FAQ говорит:эффекты памяти синхронизации в Java

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

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

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

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

+3

Недавно я натолкнулся на статью [CPU Cache Flushing Fallacy] (http://mechanical-sympathy.blogspot.com/2013/02/cpu-cache-flushing-fallacy.html), что было полезно для понимания этого лучше. –

ответ

36

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

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

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

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

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

-1

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

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

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

+0

ne0sonic, я думаю, вы неправильно поняли вопрос. Пожалуйста, поправьте меня, если я ошибаюсь. –

+1

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

+0

В исходном примере y не находится в синхронизированном блоке, поэтому, когда код выполняется, JVM не будет делать ничего особенного, чтобы гарантировать, что y обновлен, если он изменен другим потоком. Он может видеть или не видеть изменения, внесенные другими потоками. –

6

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

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

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

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

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

сказать, поток А работает на CPU А с кэш-A, поток В работает на CPU B с кэш-B,

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

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

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

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

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

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

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


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

+0

Просто примечание из моего понимания ... в (3) вы говорите: «Все эти обновления должны быть записаны в основную память». Я думаю, что это не так, потому что я читал проблемы с двойной проверкой блокировки. Запуск блока синхронизации гарантирует, что вы увидите последние данные, но в конце блока нет явного сброса. Между этим и тем фактом, что операторы могут быть переупорядочены, вы не можете ожидать согласованного представления данных, если вы также не находитесь в синхронизированном блоке. Ключевое слово volatile изменяет некоторые из этих семантик, но в основном гарантирует упорядочивание и промывку. – PSpeed

+0

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

+0

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

1

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

7

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

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

+1

Рамка Disruptor (http://code.google.com/p/disruptor/) использует этот трюк на практике. – gubby

+0

Если возможно, вы можете немного вычислить бенчмарк? Предпочтительно с кодом. – sgp15

+0

Я видел тест во время живой презентации, к сожалению, я не могу найти слайды. Но идея такова: из-за того, что данные выровнены в физической памяти и как организована кеш-память процессора, эффекты видимости памяти и их затраты на производительность могут быть связаны как с побочным эффектом, так и с полем, отмеченным как изменчивые, но также и соседние поля. Конечно, это побочный эффект, зависящий от реализации, поэтому не полагайтесь на него. Я бы ожидал, что искажение зависит только от «видимости», которая полностью законна в соответствии с JLS и не зависит от оборудования, но я не уверен на 100%. –

3

вы можете проверить jdk6.0 ДОКУМЕНТАЦИЯ http://java.sun.com/javase/6/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility

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

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

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