Многопоточное программирование подразумевает одновременный запуск нескольких потоков в одной программе. Хотя параллелизм может обеспечить значительные преимущества в производительности, он также может создавать проблемы, связанные с безопасностью потоков. Безопасность потоков относится к способности программы работать правильно и согласованно, когда несколько потоков получают доступ к общим ресурсам.
В этом руководстве по Java мы углубимся в использование переменных Java volatile для обеспечения потокобезопасности.
1. Доступ к памяти для обычных переменных
Чтобы иметь четкое представление о волатильных переменных, крайне важно иметь базовые знания о том, как работают процессоры, включая использование ими памяти, например кэша процессора.
1.1 Что такое кэш ЦП?
Кэш ЦП — это небольшой объем памяти на процессоре, в котором хранятся часто используемые данные и инструкции. Доступ к данным из кэша ЦП осуществляется гораздо быстрее, чем доступ к данным из основной памяти, которая расположена дальше от процессора и требует больше времени для доступа.
Когда ЦП необходимо получить доступ к данным или инструкциям, он сначала проверяет кэш, чтобы узнать, есть ли там данные или инструкции. Если они есть, ЦП может получить к ним быстрый и эффективный доступ. Если нет, ЦП должен извлечь данные или инструкции из основной памяти, что занимает больше времени.
1.2. Как ЦП изменяет значение переменной?
Когда программа пытается изменить переменную в основной памяти, процессор сначала проверяет, находится ли переменная уже в кэше ЦП. Если нет, переменная извлекается из основной памяти и копируется в кэш для более быстрого доступа.

Когда поток изменяет переменную в кэше ЦП, обновленное значение сначала записывается в кэш процессора, который внес изменение. Однако обновленное значение может не быть немедленно записано обратно в основную память для повышения производительности, что может привести к проблемам синхронизации, если несколько потоков обращаются к одной и той же переменной.
- Для решения этой проблемы можно использовать механизмы синхронизации, такие как блокировки или атомарные операции, чтобы гарантировать, что только один поток может получить доступ к переменной в каждый момент времени.
- Другое решение — использовать ключевое слово volatile, которое гарантирует, что изменения, внесенные в переменную volatile, немедленно станут видны всем потокам без каких-либо проблем с кэшированием или буферизацией.
2. Нестабильные переменные
Volatile variables(Изменчивые переменные) обеспечивают дополнительную гарантию того, что изменения, внесенные в переменную одним потоком, немедленно видны другим потокам. При доступе к изменчивой переменной процессор гарантирует, что все изменения этой переменной немедленно видны другим потокам и что любые кэшированные значения переменной становятся недействительными. Это делает изменчивые переменные полезными в многопоточных сценариях, где обычные переменные могут не обеспечивать достаточных гарантий синхронизации.
2.1 Синтаксис
Чтобы определить изменчивую переменную в Java, необходимо использовать ключевое слово volatile перед объявлением переменной.
public volatile int counter = 0;
2.2 Операция записи для изменчивой переменной
С переменной volatile любая операция записи в переменную немедленно записывается в основную память без кэширования или буферизации ЦП. Когда поток записывает в переменную volatile, ЦП гарантирует, что обновленное значение немедленно становится видимым для других потоков.
В следующей программе переменная-счетчик объявлена как изменчивая, поэтому операция увеличения, выполняемая над переменной «счетчик», будет выполняться непосредственно в основной памяти.
public class Main {public volatile int counter = 0;public static void main(String[] args) {Main m = new Main();m.counter+= 1;}}
2.3 Когда использовать изменчивую переменную?
Изменяемые переменные следует использовать, когда нам требуется уверенность в том, что любые изменения, внесенные в переменную одним потоком, немедленно видны другим потокам, без каких-либо проблем с кэшированием или буферизацией.
В частности, изменчивые переменные невероятно полезны, когда несколько потоков обращаются к одной и той же переменной и изменяют ее.
3. Демонстрация
В следующей программе мы создаем два потока: ThreadA и ThreadB. Оба потока получают доступ к значению переменной increment и изменяют его.
- ThreadB отвечает за увеличение переменной «increment» в цикле. ThreadB будет спать 600 миллисекунд, прежде чем возобновить цикл.
- ThreadA непрерывно отслеживает изменения переменной «increment», используя локальную переменную «x» и бесконечный цикл. Если значение «x» не совпадает со значением «increment», он выведет сообщение о том, что переменная изменилась, а затем обновит «x» для соответствия новому значению «increment».
public class Main {private static volatile int increment = 0;public static void main(String[] args){new ThreadA().start();new ThreadB().start();}static class ThreadB extends Thread {@Overridepublic void run(){while(increment < 10) {System.out.println( "Incrementing , value now is : "+ ++increment );try {Thread.sleep(600);}catch(InterruptedException e) {e.printStackTrace();}}}}static class ThreadA extends Thread {@Overridepublic void run(){int x = increment ;while(true ) {if(x != increment) {System.out.println("increment variable changed " + increment);x = increment;}}}}}
Поскольку переменная «increment» объявлена volatile, изменения, внесенные в нее одним потоком, немедленно видны всем остальным потокам. Это позволяет нескольким потокам безопасно получать доступ к переменной «increment» без необходимости явной блокировки или синхронизации.
Вывод программы:
Incrementing, value now is: 1 increment variable changed 1 Incrementing, value now is: 2 increment variable changed 2 Incrementing, value now is: 3 increment variable changed 3
В приведенном выше примере, если ключевое слово volatile удалено из переменной «increment», ThreadA может не обнаружить изменения, внесенные ThreadB в переменную «increment». Это происходит потому, что без ключевого слова volatile нет гарантии, что изменения, внесенные в переменную «increment» одним потоком, будут видны другим потокам.
Вывод программы, когда ключевое слово volatile не используется:
Incrementing, value now is: 1Incrementing, value now is: 2Incrementing, value now is: 3Incrementing, value now is: 4
4. Разница между синхронизированными и изменчивыми ключевыми словами
Особенность | синхронизированный | изменчивый |
---|---|---|
Цель | Обеспечить потокобезопасный доступ к общему ресурсу | Обеспечить видимость изменений общей переменной |
Объем | Уровень метода или блока | Переменный уровень |
Использование | Защитите критические разделы кода от одновременного доступа нескольких потоков | Укажите, что значение переменной может быть изменено разными потоками. |
Производительность | Медленный | быстрый |
Блокировка | Использует объект монитора или блокировки, чтобы разрешить доступ к синхронизированному блоку кода одновременно только одному потоку. | Не использует никаких механизмов блокировки, позволяя нескольким потокам одновременно получать доступ к переменной. |
Блокировка потока | да | нет |
5. Лучшие практики
Вот несколько рекомендаций, которые следует учитывать при использовании изменчивых переменных:
- Используйте volatile только при необходимости: Volatile следует использовать только тогда, когда нам нужно гарантировать, что значение переменной будет немедленно видно другим потокам. В общем случае лучше использовать другие механизмы синхронизации, такие как синхронизированные блоки, блокировки или атомарные переменные, если нам нужны более сложные операции.
- Избегайте сложных операций: поскольку volatile обеспечивает только видимость, а не атомарность, его не следует использовать для сложных операций, требующих атомарного выполнения нескольких операций чтения и записи.
- Используйте volatile для простых флагов и счетчиков: Изменчивые переменные хорошо подходят для простых флагов и счетчиков, к которым часто обращаются, но которые обновляются нечасто. Например, флаг, указывающий, следует ли остановить выполнение потока, или счетчик, подсчитывающий количество обработанных сообщений.
- Не полагайтесь на volatile для взаимного исключения: переменные volatile не обеспечивают взаимного исключения, поэтому мы не должны использовать их для синхронизации доступа к общим данным. Вместо этого используйте другие механизмы синхронизации, такие как блокировки, атомарные переменные или синхронизированные блоки
6. Заключение
В заключение, volatile-переменные могут быть мощным инструментом в некоторых многопоточных случаях использования. При правильном использовании они могут обеспечить простое и эффективное решение для достижения безопасности потоков и обеспечения надлежащей видимости данных между потоками.
Крайне важно понимать различия между volatile и другими механизмами синхронизации, такими как блокировки и атомарные переменные, и использовать их надлежащим образом, чтобы избежать распространенных ошибок и обеспечить правильное поведение программы.