- PVSM.RU - https://www.pvsm.ru -

Многопоточность. Модель памяти Java (часть 2)

Привет! Представляю вашему вниманию перевод второй части статьи «Java Memory Model» [1] автора Jakob Jenkov. Первая часть тут [2].

Аппаратная архитектура памяти

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

Вот упрощенная схема аппаратной архитектуры современного компьютера:
Многопоточность. Модель памяти Java (часть 2) - 1
Современный компьютер часто имеет 2 или более процессоров. Некоторые из этих процессоров также могут иметь несколько ядер. На таких компьютерах возможно одновременное выполнение нескольких потоков. Каждый процессор способен запускать один поток в любой момент времени. Это означает, что если ваше Java-приложение является многопоточным, то внутри вашей программы может быть запущен одновременно один поток на один процессор.

Каждый ЦП содержит набор регистров, которые, по существу, находятся в памяти ЦП. Процессор может выполнять операции с этими регистрами намного быстрее, чем с переменными в основной памяти. Это связано с тем, что процессор может получить доступ к этим регистрам гораздо быстрее, чем к основной памяти.

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

Компьютер также содержит область основной памяти (ОЗУ). Все процессоры могут получить доступ к основной памяти. Основная область памяти обычно намного больше, чем кэш-память процессоров.

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

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

Совмещение Java-модели памяти и аппаратной архитектуры памяти

Как уже упоминалось, Java-модель памяти и аппаратная архитектура памяти различны. Аппаратная архитектура не различает стеки потоков и кучу. На оборудовании стек потоков и куча (heap) находятся в основной памяти. Части стеков и кучи потоков могут иногда присутствовать в кэшах и внутренних регистрах ЦП. Это показано на диаграмме:
Многопоточность. Модель памяти Java (часть 2) - 2
Когда объекты и переменные могут храниться в различных областях памяти компьютера, могут возникнуть определенные проблемы. Вот две основные:
• Видимость изменений, которые произвёл поток над общими переменными.
• Состояние гонки при чтении, проверке и записи общих переменных.
Обе эти проблемы будут объяснены в следующих разделах.

Видимость общих объектов

Если два или более потока делят между собой объект без надлежащего использования volatile-объявления или синхронизации, то изменения общего объекта, сделанные одним потоком, могут быть невидимы для других потоков.

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

Следующая диаграмма иллюстрирует набросок ситуации. Один поток, работающий на левом ЦП, копирует в его кэш общий объект и изменяет значение переменной count на 2. Это изменение невидимо для других потоков, работающих на правом ЦП, поскольку обновление для count ещё не было сброшено обратно в основную память.
Многопоточность. Модель памяти Java (часть 2) - 3
Для того, чтобы решить эту проблему, вы можете использовать ключевое слово volatile [3]. Оно может гарантировать, что данная переменная считывается непосредственно из основной памяти и всегда записывается обратно в основную память при обновлении.

Состояние гонки

Если два или более потоков совместно используют один объект и более одного потока обновляют переменные в этом общем объекте, то может возникнуть состояние гонки [4].

Представьте, что поток A считывает переменную count общего объекта в кэш своего процессора. Представьте также, что поток B делает то же самое, но в кэш другого процессора. Теперь поток A прибавляет 1 к значению переменной count, и поток B делает то же самое. Теперь var1 была увеличена дважды — отдельно по +1 в кэше каждого процессора.

Если бы эти приращения были выполнены последовательно, переменная count была бы увеличена в два раза и обратно в основную память было бы записано исходное значение + 2.
Тем не менее, два приращения были выполнены одновременно без надлежащей синхронизации. Независимо от того, какой из потоков (A или B), записывает свою обновленную версию count в основную память, новое значение будет только на 1 больше исходного значения, несмотря на два приращения.

Эта диаграмма иллюстрирует возникновение проблемы с состоянием гонки, которое описано выше:
Многопоточность. Модель памяти Java (часть 2) - 4
Для решения этой проблемы вы можете использовать синхронизированный блок Java [5]. Синхронизированный блок гарантирует, что только один поток может войти в данный критический раздел кода в любой момент времени. Синхронизированные блоки также гарантируют, что все переменные, к которым обращаются внутри синхронизированного блока, будут считаны из основной памяти, и когда поток выйдет из синхронизированного блока, все обновленные переменные будут снова сброшены в основную память, независимо от того, объявлена ли переменная как volatile или нет.

Автор: mirniypirojok

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/354922

Ссылки в тексте:

[1] «Java Memory Model»: http://tutorials.jenkov.com/java-concurrency/java-memory-model.html

[2] тут: https://habr.com/ru/post/510454/

[3] ключевое слово volatile: http://tutorials.jenkov.com/java-concurrency/volatile.html

[4] состояние гонки: http://tutorials.jenkov.com/java-concurrency/race-conditions-and-critical-sections.html

[5] синхронизированный блок Java: http://tutorials.jenkov.com/java-concurrency/synchronized.html

[6] Источник: https://habr.com/ru/post/510618/?utm_source=habrahabr&utm_medium=rss&utm_campaign=510618