В течение многих лет я пытался понять часть спецификации Java, которая касается модели памяти и параллелизма. Я должен признать, что потерпел неудачу с треском. Да, я понимаю, что такое блокировки и "синхронизированы", а также wait () и notify (). И я прекрасно ими могу пользоваться, спасибо. У меня даже есть смутное представление о том, что делает «volatile». Но все это было получено не из спецификации языка, а из общего опыта.
Вот два типовых вопроса, которые я задаю. Меня не столько интересуют конкретные ответы, сколько мне нужно понять, как ответы выводятся из спецификации (или, может быть, как я пришел к выводу, что в спецификации нет ответа).




Я не буду пытаться здесь объяснять эти проблемы, а вместо этого отсылаю вас к прекрасной книге Брайана Гетца по этой теме.
Книга называется «Java Concurrency in Practice», ее можно найти на Amazon или в любом другом хорошо отсортированном магазине компьютерной литературы.
Я не буду пытаться здесь отвечать на ваши вопросы - вместо этого я перенаправлю вас к книге, которую я рекомендую для советов по этой теме: Параллелизм Java на практике.
Одно предупреждение: если здесь есть ответы находятся, ожидайте, что многие из них будут неправильными. Одна из причин, по которой я не собираюсь публиковать подробности, заключается в том, что я почти уверен, что Идентификатор ошибается, по крайней мере, в некоторых отношениях. Я не имею в виду никакого неуважения к сообществу, когда говорю, что шансы каждого, кто думает, что он может ответить на этот вопрос, на самом деле имеет достаточно строгости, чтобы сделать это правильно, практически равны нулю. (Джо Даффи недавно нашел часть модели памяти .NET, которая удивила. Если он может ошибиться, то и такие смертные, как мы, тоже могут.
Я предложу некоторое понимание только одного аспекта, потому что его часто неправильно понимают:
Есть разница между волатильностью и атомарностью. Люди часто думают, что атомарная запись является непостоянной (т.е. вам не нужно беспокоиться о модели памяти, если запись является атомарной). Это не правда.
Волатильность связана с тем, будет ли один поток, выполняющий чтение (логически, в исходном коде) «видеть» изменения, сделанные другим потоком.
Атомарность заключается в том, есть ли шанс, что если будет замечено изменение является, будет видна только часть изменения.
Например, возьмем запись в целочисленное поле. Это гарантированно атомарно, но не изменчиво. Это означает, что если у нас есть (начиная с foo.x = 0):
Thread 1: foo.x = 257;
Thread 2: int y = foo.x;
Для y возможно значение 0 или 257. Это не будет другого значения (например, 256 или 1) из-за ограничения атомарности. Однако, даже если вы знаете, что в «настенное время» код в потоке 2 выполняется после кода в потоке 1, может произойти странное кэширование, доступ к памяти «перемещается» и т. д. Сделав переменную x изменчивой, это исправит.
Остальное я оставлю на усмотрение настоящих добросовестных экспертов.
volatile, могут кэшироваться локально, поэтому разные потоки могут видеть разные значения одновременно; volatile предотвращает это (источник)long и double, хотя 64-битные JVM, вероятно, реализуют их как атомарные операцииЭто хорошая ссылка, по которой можно получить более подробную информацию:
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
Может оказаться полезным одно понятие: данные (датум) и копии.
Если вы объявляете переменную, скажем, байт, она находится где-то в памяти, в сегменте данных (грубо говоря). Где-то в памяти есть 8 бит, предназначенных для хранения этой информации.
Однако на вашем компьютере может перемещаться несколько копий этих данных. По разным техническим причинам, например локальное хранилище потока, оптимизация компилятора. А если у нас есть несколько копий, они могут быть рассинхронизированы.
Так что вы всегда должны помнить об этом. Это верно не только для полей класса java, но и для переменных cpp, записей базы данных (данные состояния записи копируются в несколько сеансов и т. д.). Переменные, их скрытые / видимые копии и тонкие проблемы с синхронизацией будут существовать вечно.
Недавно я нашел отличная статья, который объясняет изменчивость как:
First, you have to understand a little something about the Java memory model. I've struggled a bit over the years to explain it briefly and well. As of today, the best way I can think of to describe it is if you imagine it this way:
Each thread in Java takes place in a separate memory space (this is clearly untrue, so bear with me on this one).
You need to use special mechanisms to guarantee that communication happens between these threads, as you would on a message passing system.
Memory writes that happen in one thread can "leak through" and be seen by another thread, but this is by no means guaranteed. Without explicit communication, you can't guarantee which writes get seen by other threads, or even the order in which they get seen.
The Java volatile modifier is an example of a special mechanism to guarantee that communication happens between threads. When one thread writes to a volatile variable, and another thread sees that write, the first thread is telling the second about all of the contents of memory up until it performed the write to that volatile variable.
Дополнительные ссылки: http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.htmlhttp://www.javaperformancetuning.com/news/qotm030.shtml
Это не N, это N + 1! Смотрите мой ответ :)
Другие ответы, приведенные выше, абсолютно верны в том смысле, что ваш вопрос не для ловкости сердца.
Тем не менее, я понимаю вашу боль из-за того, что действительно хотите получить то, что находится под капотом - для этого я хотел бы указать вам обратно на компиляторы миров и предшественников нижнего уровня для java, то есть сборки, C и C++.
Прочтите о различных видах заграждений («заборов»). Понимание того, что такое барьер памяти и где он необходим, поможет вам интуитивно понять, что делает volatile.
Еще одна попытка представить краткое изложение того, что я понял из ответов здесь и из других источников (первая попытка была довольно далекой от базы. Я надеюсь, что эта лучше).
Модель памяти Java - это распространение значений, записанных в память в одном потоке, в другие потоки, чтобы другие потоки могли видеть их при чтении из памяти.
Короче говоря, если вы получите блокировку мьютекса, все, что написано любым потоком, который выпустил этот мьютекс раньше, будет виден вашему потоку.
Если вы читаете изменчивую переменную, все, что записано в эту изменчивую переменную до того, как вы ее прочитаете, станет видимым для потока чтения. Кроме того, любая запись в изменчивую переменную, сделанная потоком, который записывает в вашу переменную до того, как запись в вашу переменную станет видимой. Более того, в Java 1.5 любая запись, изменчивая или нет, которая произошла в любом потоке, который записал в вашу изменчивую переменную до того, как запись в вашу изменчивую переменную будет видна вам.
После создания объекта вы можете передать его другому потоку, и все конечные члены будут видны и полностью построены в новом потоке. Нет никаких аналогичных гарантий в отношении неокончательных членов. Это заставляет меня думать, что присвоение конечному члену действует как запись в изменчивую переменную (забор памяти).
Все, что поток написал до выхода из Runnable, виден потоку, который выполняет join (). Все, что поток написал до выполнения start (), будет виден созданному потоку.
Еще одна вещь, о которой стоит упомянуть: у изменчивых переменных и синхронизации есть функция, о которой редко упоминается: помимо очистки кеша потоков и предоставления доступа по одному потоку, они также не позволяют компилятору и ЦП переупорядочивать чтения и записи через границу синхронизации.
Ничего из этого не ново, и в других ответах это сказано лучше. Я просто хотел написать это, чтобы прояснить себе голову.
Это объясняет использование городов (потоков) и планет (основная память).
http://mollypages.org/tutorials/javamemorymodel.mp
Прямых рейсов из города в город нет.
Вам нужно сначала отправиться на другую планету (в данном случае на Марс), а затем в другой город на вашей родной планете. Итак, из Нью-Йорка в Токио нужно ехать:
Нью-Йорк -> Марс -> Токио
Теперь замените NYC и Tokyo двумя потоками, Mars на основную память и полеты как получение / освобождение блокировок, и у вас есть JMM.
Схема модели памяти JVM высокого уровня

Образец кода
class MainClass {
void method1() { //<- main
int variable1 = 1;
Class1 variable2 = new Class1();
variable2.method2();
}
}
class Class1 {
static Class2 classVariable4 = new Class2();
int instanceVariable5 = 0;
Class2 instanceVariable6 = new Class2();
void method2() {
int variable3 = 3;
}
}
class Class2 { }
*Заметки:
thread stack содержит только локальные переменныеheap, даже если они являются примитивами.What does "volatile" do, exactly?
Are writes to variable atomic? Does it depend on variable's type?
Спасибо за ссылку! Это не заменяет книгу (которую я получу), но она дала мне одно понимание: я привык думать о синхронизации, а модель памяти больше связана с переупорядочением. Мне нужно научиться различать эти два и думать о последнем.