JavaRush /Курсы /JSP & Servlets /Java Memory Model

Java Memory Model

JSP & Servlets
18 уровень , 2 лекция
Открыта

Знакомство с Java Memory Model

Модель памяти Java (Java Memory Model, JMM) описывает поведение потоков в среде исполнения Java. Модель памяти — часть семантики языка Java, и описывает, на что может и на что не должен рассчитывать программист, разрабатывающий ПО не для конкретной Java-машины, а для Java в целом.

Исходная модель памяти Java (к которой, в частности, относится “потоколокальная память”), разработанная в 1995 году, считается неудачной: многие оптимизации невозможно провести, не потеряв гарантию безопасности кода. В частности, есть несколько вариантов написать многопоточного “одиночку”:

  • либо каждый акт доступа к одиночке (даже когда объект давно создан, и ничего уже не может измениться) будет вызывать межпоточную блокировку;
  • либо при определенном стечении обстоятельств система выдаст недостроенного одиночку;
  • либо при определенном стечении обстоятельств система создаст два одиночки;
  • либо конструкция будет зависеть от особенностей поведения той или иной машины.

Поэтому механизм работы памяти был переработан. В 2005 году, с выходом Java 5 был презентован новый подход, который был еще улучшен с выходом Java 14.

В основе новой модели лежат три правила:

Правило № 1: однопоточные программы исполняются псевдопоследовательно. Это значит: в реальности процессор может выполнять несколько операций за такт, заодно изменив их порядок, однако все зависимости по данным остаются, так что поведение не отличается от последовательного.

Правило № 2: нет невесть откуда взявшихся значений. Чтение любой переменной (кроме не-volatile long и double, для которых это правило может не выполняться) выдаст либо значение по умолчанию (ноль), либо что-то, записанное туда другой командой.

И правило № 3: остальные события выполняются по порядку, если связаны отношением строгого частичного порядка “выполняется прежде” (happens before).

Happens before

Лесли Лэмпорт придумал понятие Happens before. Это отношение строгого частичного порядка, введенное между атомарными командами (++ и -- не атомарны) и не означающее “физически прежде”.

Оно говорит о том, что вторая команда будет “в курсе” изменений, проведенных первой.

Happens before

Например, одно выполняется прежде другого для таких операций:

Синхронизация и мониторы:

  • Захват монитора (метод lock, начало synchronized) и все, что происходит в том же потоке после него.
  • Возврат монитора (метод unlock, конец synchronized) и все, что происходит в том же потоке перед ним.
  • Возврат монитора и последующий захват другим потоком.

Запись и чтение:

  • Запись в любую переменную и последующее чтение ее же в одном потоке.
  • Все, что в том же потоке перед записью в volatile-переменную, и сама запись. volatile-чтение и все, что в том же потоке после него.
  • Запись в volatile-переменную и последующее считывание ее же. Volatile-запись взаимодействует с памятью так же как и возврат монитора, а чтение как захват. Получается, что если один поток записал в volatile-переменную, а второй обнаружил это, все, что предшествует записи, выполняется раньше всего, что идет после чтения; смотри рисунок.

Обслуживание объекта:

  • Статическая инициализация и любые действия с любыми экземплярами объектов.
  • Запись в final-поля в конструкторе и все, что после конструктора. Как исключение – соотношение happens-before не соединяется транзитивно с другими правилами и поэтому может вызвать межпоточную гонку.
  • Любая работа с объектом и finalize().

Обслуживание потока:

  • Запуск потока и любой код в потоке.
  • Зануление переменных, относящихся к потоку, и любой код в потоке.
  • Код в потоке и join(); код в потоке и isAlive() == false.
  • interrupt() потока и обнаружение факта остановки.

Нюансы работы Happens before

Освобождение (releasing) монитора happens-before происходит прежде, чем получение (acquiring) того же монитора. Стоит обратить внимание, что именно освобождение, а не выход, то есть за безопасность при использовании wait можно не беспокоиться.

Рассмотрим, как это знание поможет нам исправить наш пример. В данном случае все очень просто: достаточно убрать внешнюю проверку и оставить синхронизацию как есть. Теперь второй поток гарантированно увидит все изменения, потому что он получит монитор только после того, как другой поток его отпустит. А так как он его не отпустит, пока все не проинициализирует, мы увидем все изменения сразу, а не по отдельности:


public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

Запись в volatile переменную happens-before чтение из той же переменной. То изменение, которое мы внесли, конечно, исправляет некорректность, но возвращает того, кто написал изначальный код, туда, откуда он пришел — к блокировке каждый раз. Спасти может ключевое слово volatile. Фактически, рассматриваемое утверждение значит, что при чтении всего, что объявлено volatile, мы всегда будем получать актуальное значение.

Кроме того, как я говорил раньше, для volatile полей запись всегда (в том числе long и double) является атомарной операцией. Еще один важный момент: если у вас есть volatile сущность, имеющая ссылки на другие сущности (например, массив, List или какой-нибудь еще класс), то всегда “свежей” будет только ссылка на саму сущность, но не на все, в нее входящее.

Итак, обратно к нашим Double-locking баранам. С использованием volatile исправить ситуацию можно так:


public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

Тут у нас по-прежнему есть блокировка, но только в случае, если data == null. Остальные случаи мы отсеиваем, используя volatile read. Корректность обеспечивается тем, что volatile store happens-before volatile read, и все операции, которые происходят в конструкторе, видны тому, кто читает значение поля.

Комментарии (12)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Pavel T Уровень 32
28 января 2025
Просто чуть-чуть для контекста, может помогу кому-то. Часто вопрос спрашивают на собеседованиях. Особенно полезно знать, чтобы козырнуть знаниями, когда volatile нас не спасает. Гуглите) Почему нужна Java Memory Model (JMM)? До того как Java стала той быстрой платформой, которую мы знаем сегодня, разработчики столкнулись с проблемой производительности. Для её решения был введён целый ряд оптимизаций, позволяющих ускорять выполнение программ. Как работают оптимизации? Рассмотрим пример: x = 0; x += 10; x += 5; Компилятор видит, что все операции относятся к одной переменной x, и оптимизирует их, превратив в одну: x = 15; Это значит, что порядок операций, который вы написали в коде, может быть изменён в байт-коде для повышения производительности. Где возникает проблема? Теперь представим, что наша программа работает в многопоточной среде, где несколько потоков одновременно обращаются к переменной x. Если операции над x могут быть выполнены в другом порядке, логика программы может быть нарушена. Например: Thread 1: x = 10; Thread 2: x = 15; В зависимости от порядка операций поток 1 может увидеть не то значение, которое было записано потоком 2. Решение: Java Memory Model Чтобы избежать таких проблем, была создана Java Memory Model (JMM). Она определяет: Когда и в каком порядке один поток видит изменения, внесённые другим. Какие операции можно оптимизировать и в каких случаях. Основные принципы JMM JMM вводит понятие "happens-before" — это правило, которое гарантирует, что одна операция произойдёт раньше другой. Например: Запись переменной в одном потоке может быть гарантированно видна другому потоку. Для синхронизации используются ключевые слова, такие как volatile, synchronized, и классы из java.util.concurrent.
Олег Уровень 106 Expert
12 сентября 2024
Я ничего не понял
Иван Корниенко Уровень 109
18 июня 2024
Источники: 1. тык 2. тык 3. тык
Андрей Уровень 109
7 июня 2024
Рассмотрим, как это знание поможет нам исправить наш пример. Какой пример? Откуда он появился вдруг?
mega478 Уровень 24
24 октября 2023
...создаст два одиночки; - Русский речь меня покинул!
Олег Уровень 111 Expert
25 августа 2023
Ну кто так излагает:"Запись в volatile переменную happens-before чтение из той же переменной. То изменение, которое мы внесли, конечно, исправляет некорректность, но возвращает того, кто написал изначальный код, туда, откуда он пришел — к блокировке каждый раз. Спасти может ключевое слово volatile. Фактически, рассматриваемое утверждение значит, что при чтении всего, что объявлено volatile, мы всегда будем получать актуальное значение." Как-будто машинный перевод редактировал первокласник. Можно было хотя бы так изложить: "Использование ключевого слова volatile позволяет обеспечить последовательность операций в многопоточной среде. Когда переменная помечается как volatile, это указывает, что её значения могут изменяться разными потоками. Основной эффект заключается в том, что запись значения в переменную, помеченную как volatile, "происходит до" (happens-before) любого последующего чтения этого значения из другого потока. Таким образом, любое значение, записанное в volatile переменную одним потоком, будет гарантированно видимо другим потокам, которые читают это значение. Это обеспечивает предсказуемость и последовательность операций чтения и записи в среде с несколькими потоками. Однако необходимо отметить, что хотя использование ключевого слова volatile может помочь обеспечить видимость изменений между потоками, оно может быть ограничено в сложных сценариях синхронизации. В некоторых случаях более сложные механизмы синхронизации, такие как блокировки, могут потребоваться для обеспечения корректности и безопасности программы."
mega478 Уровень 24
24 октября 2023
По моему этот текст даже не редактировали, после кривого автоперевода!
6 мая 2025
у тебя лайкосов больше чем у лекции
Anonymous #311541 Уровень 1
31 июля 2022
хотелось бы побольше примеров из жизни. По сути в конце написан код очень похожий на trade-safe код для создания синглтона. Вот уж никогда не думал что в этом коде использовался принцип happens before. Почему то никто об этом не упоминает когда разбирает этот код. А что было бы еслиб не было этого принципа? Можно привести код в котором чтото ломается из-за того что нет happens before
Anonymous #3076791 Уровень 3
14 августа 2022
я думал, я один сюда дошел :)
Oleg Khilko Уровень 51
16 августа 2022
1) Многопоточность в Java: основы 2) Многопоточность в Java: средства стандартной библиотеки Рекомендую посмотреть эти две лекции чтобы лучше понять.
hint1k Уровень 51
6 мая 2023
Олег, это очень слабенькие лекции. 1) нити называет потоками, 2) бедный язык с кучей канцелярита, 3) плохо поставленная речь, 4) не умеет выступать перед аудиторией, 5) не использует визуалицию там где она явно нужна (например объяснение дэдлока) ну и т.д. Сравни вот например чел на инглише объясняет туже самую тему видео Этот англоговорящий товарищ: 1) явно умеет выступать перед аудиторией - жесты, взгляд, речь 2) очень хорошо говорит, без мусорных слов и канцелярщины 3) использует визуализацию по максимуму. и т.д. Причем разница между этими двумя видна даже если не знать русского и английского языка. Т.к. через невербальное общение идет около 80% информации.