Знакомство с JMM. Ключевое слово volatile
Дорогу осилит идущийКонкурентность в целом и многопоточность в частности – достаточно многогранная и сложная область, в которой каждый уровень взаимодействия с многопоточной средой – от выполнения инструкций процессором до особенностей использования конкурентности в высокоуровневом языке программирования (в нашем случае в Java) – может оказывать свое влияние на эту среду, внося свои коррективы.
Большой плюс Java в том, что она предоставляет достаточно гибкий и прозрачный интерфейс для взаимодействия с потоками (на фоне, скажем, C++), который скрывает от программиста многие тонкости.
Однако некоторые нюансы (преимущественно, связанные с оптимизацией) на уровне компиляции Java-кода и его выполнения процессором могут приводить к тому, что, казалось бы, однозначно описанная логика в многопоточной среде работает вовсе не так, как ожидает программист.
Описанная (очень поверхностно) проблематика выше привела к разработке JMM – Java Memory Model – модели, описывающая поведение потоков в Java. По сути, это набор правил, который определяют, какие гарантии выполнения программист имеет, работая с многопоточной средой. Зачастую в рамках знакомства с JMM описывают и возможные проблемы, которые могут возникнуть при работе в такой среде.
На данном этапе мы не будем сколь-либо глубоко знакомиться с JMM. Возможно, под конец раздела «Многопоточность» под это будет выделена отдельная статья, но полезность этих знаний для junior-специалистов остается под вопросом. Однако в статье мы будем обращаться к проблемам, которые JMM призван решать. Более подробный поиск по ним легко наводит на статьи по углубленному знакомству с JMM.
Целью же текущей статьи является знакомство с новым для нас синтаксисом – ключевым словом volatile. Это один из инструментов обеспечения прозрачной работы кода в многопоточной среде, предоставляемых JMM. Подробнее разберемся ниже.
Проблематика
Кэши процессора. Процессор, выполняющий написанный вами код, имеет ряд собственных кэшей. Данные кэши могут сохранять значения некоторых переменных, которые используются потоком, с целью дальнейшего использования в рамках этого же потока.
Такой подход позволяет нарастить производительность, но имеет существенный недостаток: переменные, которые используются несколькими потоками одновременно тоже могут попасть в кэш процессора. И тогда изменение значения переменной в Потоке 1 может остаться «незамеченным» в Потоке 2, потому что Поток 2 использует значение переменой из кэша и не проверяет реальное значение переменной в памяти.
Следствием может быть некорректное и непредсказуемое поведение программы. Для наглядности можете представить, что Java вдруг стала игнорировать случайно взятую часть операций присваивания в вашем коде. Пример не тождественный, но позволяющий оценить уровень проблемы в проекции на однопоточную среду. Для более подробного разбора гуглим «Visibility Java».
Оптимизации кода. Компилятор и процессор могут менять порядок операций в коде для повышения производительности. Таким образом, код, который выполняет процессор, может быть не совсем эквивалентен коду, который написал программист. И если для однопоточной среды это не играет роли, то при взаимодействии нескольких потоков может приводить к непредсказуемому поведению. Для более подробного разбора гуглим «Reordering Java».
!NB: В проблемы, решаемые volatile часто включают проблему атомарности записи.
Для более подробного разбора гуглим «Atomicity JMM».
Если очень кратко: в ряде случаев long и double не могут быть записаны в память одной операцией. Т.е. сначала запишется первая половина числа (в двоичной системе счисления), потом вторая. Возможна ситуация, когда другой поток считает число, когда оно записано лишь наполовину.
Почему-то популярно мнение, по крайней мере, в русскоязычном сообществе, что использование volatile может решить эту проблему. Но это не так.
Что делает ключевое слово volatile
Как можно догадаться, использование volatile определяет следующее:
1. Для данной переменной не будут использоваться кэши процессора. Таким образом, всем потокам в любой момент времени будет известно только одно – актуальное – значение volatile-переменной;
2. Для volatile-переменных гарантируется, что их чтение и запись относительно друг друга происходят в порядке, заданном программистом. Лаконично сформулировать сложно, но если вы ознакомились с проблемой Reordering в JMM, то легко поймете, о чем идет речь.
Таким образом, использование volatile для переменных дает почти ту же прозрачность выполнения, которая привычна для нас при программировании в однопоточной среде. Безусловно, многие проблемы взаимодействия потоков остаются, но они сводятся, преимущественно, к логике самой программы, а не низкоуровневым особенностям обработки и исполнения кода.
Цена комфорта – падение производительности. Поэтому использование volatile должно быть осознанным. В большинстве задач, даже в многопоточных средах, в его использовании нет необходимости.
Наконец, рассмотрим простейший пример использования:
public class VolatileExample {
private volatile boolean flag = false; // Объявление volatile
// переменной (поля)
public void writeFlag() {
flag = true; // Запись значения в volatile переменную
}
public void readFlag() {
if (flag) { // Чтение значения из volatile переменной
System.out.println("Flag is true");
}
}
}
Обратите внимание: говоря о volatile-переменной не идет речи о переменной в привычном для Java смысле – переменной метода. volatile может быть применен только к полям класса. volatile для локальных переменных не имеет смысла – такие переменные существуют только в рамках своего потока выполнения.
Еще одной особенностью является зона применения volatile. Очень важно понимать, что volatile для полей примитивных типов гарантирует все вышесказанное (видимость и упорядоченность) для значения переменной (что логично).
А для ссылочных типов гарантирует то же самое для ссылки (тоже логично, но не всем очевидно). Именно ссылки, а не объекта, на который эта ссылка указывает. volatile никак не регламентирует механизмы изменения полей объекта по ссылке. Однако поля в таком объекте тоже могут быть объявлены как volatile.
В заключение стоит сказать, что ключевое слово volatile – полезный инструмент, позволяющий «отключать» некоторые низкоуровневые оптимизациями кода. Но он не решает глобальной проблемы взаимодействия потоков. И не заменяет собой синхронизацию и другие механизмы регуляции взаимодействия, с которыми мы познакомимся в дальнейшем.
Но именно грамотное и уместное использование каждого из таких инструментов (или механизмов) делает многопоточность эффективной.
С теорией на сегодня все!
Несмотря на то, что урок знакомит с новым синтаксисом, в рамках простых задач отследить разницу между переменными с использованием volatile и без него не так просто. Поэтому пусть данный урок останется теоретическим.

Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)
Канал: https://t.me/+relA0-qlUYAxZjI6
Мой тг: https://t.me/ironicMotherfucker
Дорогу осилит идущий!