Модель памяти Java - кто-то может это объяснить?
В течение многих лет я пытался понять ту часть спецификации Java, которая касается модели памяти и параллелизма. Я должен признать, что я с треском провалился. Да, я понимаю о блокировках и "синхронизированных" и wait() и notify(). И я могу использовать их просто отлично, спасибо. У меня даже есть смутное представление о том, что делает "изменчивый". Но все это происходило не из языковой спецификации, а из общего опыта.
Вот два примера вопросов, которые я задаю. Меня не очень интересуют конкретные ответы, так как мне нужно понять, как ответы получены из спецификации (или, возможно, я понял, что в спецификации нет ответа).
- Что именно делает "volatile"?
- Является ли запись в переменную atomic? Зависит ли это от типа переменной?
10 ответов
Я не собираюсь на самом деле отвечать на ваши вопросы здесь - вместо этого я перенаправлю вас к книге, которую я вижу рекомендованной для совета по этой теме: параллелизм 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
предотвращает это ( источник) - запись в переменные 32 бит или меньше гарантированно будет атомарной ( подразумевается здесь); не так для
long
а такжеdouble
хотя 64-битные JVM, вероятно, реализуют их как атомарные операции
Я не буду пытаться объяснить эти проблемы здесь, но вместо этого направлю вас к превосходной книге Брайана Гетца по этому вопросу.
Книга называется "Параллелизм Java на практике", ее можно найти на Amazon или в любом другом магазине компьютерной литературы.
Это хорошая ссылка, которая может дать вам немного подробной информации:
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
Недавно я нашел отличную статью, которая объясняет изменчивость как:
Во-первых, вы должны кое-что понять о модели памяти Java. Я изо всех сил пытался объяснить это кратко и хорошо. На сегодняшний день, лучший способ описать это, если представить это так:
Каждый поток в Java происходит в отдельном пространстве памяти (это явно не соответствует действительности, так что потерпите меня на этом).
Вам нужно использовать специальные механизмы, чтобы гарантировать, что связь между этими потоками происходит, как в системе передачи сообщений.
Операции записи в память, происходящие в одном потоке, могут "просачиваться" и просматриваться другим потоком, но это никоим образом не гарантировано. Без явного общения вы не можете гарантировать, какие записи будут видны другим потокам, или даже порядок их просмотра.
Модификатор Java volatile является примером специального механизма, гарантирующего взаимодействие между потоками. Когда один поток записывает в переменную переменной, а другой поток видит эту запись, первый поток сообщает второму обо всем содержимом памяти до тех пор, пока не выполнит запись в эту переменную переменную.
Дополнительные ссылки: http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html http://www.javaperformancetuning.com/news/qotm030.shtml
Схема модели памяти 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
даже они примитивны
Что именно делает "volatile"?
Записывает в переменную атомарно? Зависит ли это от типа переменной?
Другие ответы, приведенные выше, абсолютно верны в том смысле, что ваш вопрос не для чутья сердца.
Тем не менее, я понимаю вашу боль в том, что вы действительно хотите получить то, что у вас под капотом - для этого я хотел бы указать вам обратно на компиляторы миров и низкоуровневые предшественники java - то есть ассемблер, C и C++.
Читайте о различных видах барьеров ("заборы"). Понимание того, что такое барьер памяти и где это необходимо, поможет вам понять, что делает изменчивый.
Это объясняет использование городов (потоков) и планет (основная память).
http://mollypages.org/tutorials/javamemorymodel.mp
Прямых рейсов из города в город нет.
Вам нужно сначала отправиться на другую планету (в данном случае на Марс), а затем в другой город на вашей родной планете. Итак, из Нью-Йорка в Токио нужно ехать:
Нью-Йорк -> Марс -> Токио
Теперь замените Нью-Йорк и Токио двумя потоками, Марс на основную память и полеты как получение / освобождение блокировок, и у вас есть JMM.
Еще одна попытка дать краткое изложение того, что я понял из ответов здесь и из других источников (первая попытка была довольно далека от базы. Надеюсь, что эта лучше).
Модель памяти Java предназначена для передачи значений, записанных в память в одном потоке, в другие потоки, чтобы другие потоки могли видеть их при чтении из памяти.
Короче говоря, если вы получите блокировку для мьютекса, все, что написано любым потоком, который освободил этот мьютекс раньше, будет видно вашему потоку.
Если вы читаете переменную volatile, все, что записано в эту переменную перед тем, как вы ее прочитаете, будет видно потоку чтения. Кроме того, любая запись в переменную volatile выполняется потоком, который записывает в вашу переменную до записи в вашу переменную. Более того, в Java 1.5 любая запись вообще, изменчивая или нет, которая происходила в любом потоке, который записывал в вашу переменную переменную до записи в вашу переменную переменную, будет видна вам.
После того, как объект создан, вы можете передать его другому потоку, и все конечные члены будут видимы и полностью созданы в новом потоке. Нет никаких аналогичных гарантий относительно не финальных участников. Это заставляет меня думать, что присваивание конечному члену действует как запись в переменную volatile (ограничение памяти).
Все, что поток написал до выхода из Runnable, видно потоку, выполняющему join(). Все, что поток написал перед выполнением start(), будет видно порожденному потоку.
Еще одна вещь, которую стоит упомянуть: переменные и синхронизация имеют функцию, которая редко упоминается: помимо очистки кеша потоков и предоставления доступа по одному за раз, они также не позволяют компилятору и ЦП переупорядочивать операции чтения и записи через границу синхронизации.
Ничто из этого не является новым, и другие ответы изложили это лучше. Я просто хотел написать это, чтобы очистить голову.
Может пригодиться одно понятие: данные (данные) и копии.
Если вы объявляете переменную, скажем, байт, она находится где-то в памяти, в сегменте данных (грубо говоря). В памяти есть 8 битов, предназначенных для хранения этой информации.
Однако может быть несколько копий этих данных, перемещающихся на вашем компьютере. По различным техническим причинам, например, локальное хранилище потока, оптимизация компилятора. И если у нас есть несколько копий, они могут быть не синхронизированы.
Так что вы всегда должны помнить об этом. Это верно не только для полей класса java, но и для переменных cpp, записей базы данных (данные о состоянии записи копируются в несколько сеансов и т. Д.). Переменные, их скрытые / видимые копии и тонкие проблемы синхронизации будут существовать всегда.