Русский
Русский
English
Статистика
Реклама

Java memory model

Из песочницы Многопоточность. Модель внутренней памяти Java простыми словами

10.07.2020 14:07:02 | Автор: admin
Привет, Хабр! Представляю вашему вниманию перевод статьи Java Memory Model автора Jakob Jenkov.

image

Прохожу обучение по Java и понадобилось изучить статью Java Memory Model. Перевёл её для лучшего понимания, ну а чтоб добро не пропадало решил поделиться с сообществом. Думаю, для новичков будет полезно, и если кому-то понравится, то переведу остальное.

Модель памяти Java, используемая внутри JVM, делит память на стеки потоков (thread stack) и кучу (heap). Эта диаграмма иллюстрирует модель памяти Java с логической точки зрения:

image

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

Стек потока содержит все локальные переменные для каждого выполняемого метода (всех методов в стеке вызовов). Поток может получить доступ только к своему стеку потоков. Локальные переменные, невидимы для всех других потоков, кроме потока, который их создал. Даже если два потока выполняют один и тот же код, они всё равно будут создавать локальные переменные этого кода в своих собственных стеках. Таким образом, каждый поток имеет свою версию каждой локальной переменной.

Все локальные переменные примитивных типов (boolean, byte, short, char, int, long, float, double) полностью хранятся в стеке потоков и не видны другим потокам. Один поток может передать копию примитивной переменной другому потоку, но не может совместно использовать примитивную локальную переменную.

Куча содержит все объекты, созданные в вашем приложении Java, независимо от того, какой поток создал объект. К этому относятся и версии объектов примитивных типов (например, Byte, Integer, Long и т.д.). Неважно, был ли объект создан и присвоен локальной переменной или создан как переменная-член другого объекта, он хранится в куче.

Вот диаграмма, иллюстрирующая стек вызовов и локальные переменные, которые хранятся в стеках потоков, а также объекты, которые хранятся в куче:

image

Локальная переменная может быть примитивного типа, в этом случае она полностью хранится в стеке потока.

Локальная переменная также может быть ссылкой на объект. В этом случае ссылка (локальная переменная) хранится в стеке потоков, но сам объект хранится в куче.

Объект может содержать методы, и эти методы могут содержать локальные переменные. Эти локальные переменные также хранятся в стеке потоков, даже если объект, которому принадлежит метод, хранится в куче.

Переменные-члены объекта хранятся в куче вместе с самим объектом. Это верно как в случае, когда переменная-член имеет примитивный тип, так и в том случае, если она является ссылкой на объект.

Переменные статического класса также хранятся в куче вместе с определением класса.

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

Вот диаграмма, иллюстрирующая пункты выше:

image

Два потока имеют набор локальных переменных. Local Variable 2 указывает на общий объект в куче (Object 3). То есть каждый из потоков имеет свою копию локальной переменной со своей ссылкой. Таким образом, две разные ссылки указывают на один и тот же объект в куче.

Обратите внимание, что общий Object 3 имеет ссылки на Object 2 и Object 4 как переменные-члены (показано стрелками). Через эти ссылки два потока могут получить доступ к Object 2 и Object 4.

На диаграмме также показана локальная переменная (Local variable 1). Каждая её копия содержит разные ссылки, которые указывают на два разных объекта (Object 1 и Object 5), а не на один и тот же. Теоретически оба потока могут обращаться как к Object 1, так и к Object 5, если они имеют ссылки на оба этих объекта. Но на диаграмме выше каждый поток имеет ссылку только на один из двух объектов.

Итак, какой же Java-код может являться следствием этих иллюстраций? Ну, такой же простой код, как код ниже:

Public class MyRunnable implements Runnable() {    public void run() {        methodOne();    }    public void methodOne() {        int localVariable1 = 45;        MySharedObject localVariable2 =            MySharedObject.sharedInstance;        //... do more with local variables.        methodTwo();    }    public void methodTwo() {        Integer localVariable1 = new Integer(99);        //... do more with local variable.    }}

public class MySharedObject {    //статическая переменная, указывающая на экземпляр MySharedObject    public static final MySharedObject sharedInstance =        new MySharedObject();    // переменные-члены, указывающие на два объекта в куче    public Integer object2 = new Integer(22);    public Integer object4 = new Integer(44);    public long member1 = 12345;    public long member2 = 67890;}

Метод run() вызывает methodOne(), а methodOne() вызывает methodTwo().

methodOne() объявляет примитивную локальную переменную (localVariable1) типа int и локальную переменную (localVariable2), которая является ссылкой на объект.

Каждый поток, выполняющий методOne(), создаст свою собственную копию localVariable1 и localVariable2 в своих соответствующих стеках. Переменные localVariable1 будут полностью отделены друг от друга, находясь в стеке каждого потока. Один поток не может видеть, какие изменения вносит другой поток в свою копию localVariable1.

Каждый поток, выполняющий методOne(), также создает свою собственную копию localVariable2. Однако две разные копии localVariable2 в конечном итоге указывают на один и тот же объект в куче. Дело в том, что localVariable2 указывает на объект, на который ссылается статическая переменная sharedInstance. Существует только одна копия статической переменной, и эта копия хранится в куче. Таким образом, обе копии localVariable2 в конечном итоге указывают на один и тот же экземпляр MySharedObject. Экземпляр MySharedObject также хранится в куче. Он соответствует Object 3 на диаграмме выше.

Обратите внимание, что класс MySharedObject также содержит две переменные-члены. Сами переменные-члены хранятся в куче вместе с объектом. Две переменные-члены указывают на два других объекта Integer. Эти целочисленные объекты соответствуют Object 2 и Object 4 на диаграмме.

Также обратите внимание, что methodTwo() создает локальную переменную с именем localVariable1. Эта локальная переменная является ссылкой на объект типа Integer. Метод устанавливает ссылку localVariable1 для указания на новый экземпляр Integer. Ссылка будет храниться в своей копии localVariable1 для каждого потока. Два экземпляра Integer будут сохранены в куче и, поскольку метод создает новый объект Integer при каждом выполнении, два потока, выполняющие этот метод, будут создавать отдельные экземпляры Integer. Они соответствуют Object 1 и Object 5 на диаграмме выше.

Обратите также внимание на две переменные-члены в классе MySharedObject типа long, который является примитивным типом. Поскольку эти переменные являются переменными-членами, они все еще хранятся в куче вместе с объектом. В стеке потоков хранятся только локальные переменные.
Подробнее..
Категории: Java , Java memory model

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

12.07.2020 18:13:01 | Автор: admin
Привет, Хабр! Представляю вашему вниманию перевод второй части статьи Java Memory Model автора Jakob Jenkov. Первая часть тут.

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


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

Вот упрощенная схема аппаратной архитектуры современного компьютера:

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

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

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

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

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

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

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


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

Когда объекты и переменные могут храниться в различных областях памяти компьютера, могут возникнуть определенные проблемы. Вот две основные:
Видимость изменений, которые произвёл поток над общими переменными.
Состояние гонки при чтении, проверке и записи общих переменных.
Обе эти проблемы будут объяснены в следующих разделах.

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


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

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

Следующая диаграмма иллюстрирует набросок ситуации. Один поток, работающий на левом ЦП, копирует в его кэш общий объект и изменяет значение переменной count на 2. Это изменение невидимо для других потоков, работающих на правом ЦП, поскольку обновление для count ещё не было сброшено обратно в основную память.

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

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


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

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

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

Эта диаграмма иллюстрирует возникновение проблемы с состоянием гонки, которое описано выше:

Для решения этой проблемы вы можете использовать синхронизированный блок Java. Синхронизированный блок гарантирует, что только один поток может войти в данный критический раздел кода в любой момент времени. Синхронизированные блоки также гарантируют, что все переменные, к которым обращаются внутри синхронизированного блока, будут считаны из основной памяти, и когда поток выйдет из синхронизированного блока, все обновленные переменные будут снова сброшены в основную память, независимо от того, объявлена ли переменная как volatile или нет.
Подробнее..
Категории: Java , Java memory model

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru