Доступ к памяти для изменчивых переменных и потокобезопасность в Java

Многопоточное программирование подразумевает одновременный запуск нескольких потоков в одной программе. Хотя параллелизм может обеспечить значительные преимущества в производительности, он также может создавать проблемы, связанные с безопасностью потоков. Безопасность потоков относится к способности программы работать правильно и согласованно, когда несколько потоков получают доступ к общим ресурсам.

В этом руководстве по 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 и другими механизмами синхронизации, такими как блокировки и атомарные переменные, и использовать их надлежащим образом, чтобы избежать распространенных ошибок и обеспечить правильное поведение программы.

Прокрутить вверх