Изменение переменной, сделанное в одном потоке, не сразу видно другому потоку. Исправить это можно с помощью volatile — ключевого слова, которое ставится перед переменной. В отличие от слова synchronized, которое применимо для метода или для блока кода, слово volatile применимо только для переменной. volatile — это более слабый вариант синхронизации, который иногда бывает достаточным.
Рассмотрим пример, показывающий, что изменение переменной в одном потоке действительно не сразу видно другому потоку (или даже никогда не видно).
Некорректный код
Пусть поток VolatileTest длится до тех пор, пока keepRunning=true:
public class VolatileTest extends Thread { boolean keepRunning = true; public void run() { while (keepRunning) { } System.out.println("Thread terminated."); } }
Запустим поток VolatileTest из основного потока main, подождем секунду и изменим значение переменной keepRunning на false:
public class VolatileTest extends Thread { boolean keepRunning = true; public void run() { while (keepRunning) { } System.out.println("Thread terminated."); } public static void main(String[] args) throws InterruptedException { VolatileTest t = new VolatileTest(); t.start(); Thread.sleep(1000); t.keepRunning = false; System.out.println("keepRunning set to false."); } }
Казалось бы, поток VolatileTest должен завершиться через секунду, когда условие цикла поменяется. Но нет, поток не завершается никогда (на моем ПК точно).
Имеем такой вывод в консоль:
keepRunning set to false.
Программа не завершается.
Объясняется это тем, что при отсутствии синхронизации JVM может преобразовать код:
while (keepRunning) {}
в код:
if (keepRunning) while (true) {}
Эти преобразования делаются ради оптимизации. И программа никогда не заканчивается.
Исправить ситуацию может уже упомянутая синхронизация либо ключевое слово volatile.
Вариант с volatile
Проще всего поставить ключевое слово volatile перед переменной keepRunning:
public class VolatileTest extends Thread { volatile boolean keepRunning = true; public void run() { while (keepRunning) { } System.out.println("Thread terminated."); } public static void main(String[] args) throws InterruptedException { VolatileTest t = new VolatileTest(); t.start(); Thread.sleep(1000); t.keepRunning = false; System.out.println("keepRunning set to false."); } }
volatile гарантирует, что все изменения значения keepRunning, сделанные в одном потоке, сразу же доступны для чтения в другом потоке. Иными словами, любой поток всегда видит последнее значение переменной volatile.
Теперь имеем вывод в консоль:
keepRunning set to false. Thread terminated.
Программа длится секунду и завершается.
Вариант с синхронизацией (synchronized)
Можно заставить считывать значение переменной с помощью метода getKeepRunning(), а писать с помощью setKeepRunning(). При этом ключевое слово synchronized перед методами гарантирует, что два потока одновременно не могут войти в эти методы. Это значит, что когда основной поток заходит в setKeepRunning(), допуск в VolatileTest-потока в getKeepRunning() приостанавливается до завершения setKeepRunning(). Когда VolatileTest-поток попадет в getKeepRunning(), он прочитает уже обновленное значение:
public class VolatileTest extends Thread { boolean keepRunning = true; public void run() { while (getKeepRunning()) { } System.out.println("Thread terminated."); } synchronized void setKeepRunning() { keepRunning = false; } synchronized boolean getKeepRunning() { return keepRunning; } public static void main(String[] args) throws InterruptedException { VolatileTest t = new VolatileTest(); t.start(); Thread.sleep(1000); t.setKeepRunning(); System.out.println("keepRunning set to false."); } }
Вывод в консоль:
keepRunning set to false. Thread terminated.
Программа завершается через секунду, результат такой же — корректный.
Итоги
В данном примере проблему решает как ключевое слово volatile, так и synchronized, но только потому, что keepRunning = true — атомарная операция. Для нее достаточно слова volatile. Если бы мы в двух потоках делали, например, увеличение переменной счетчика counter++, то слово volatile уже бы не помогло. Потому что counter++ — не атомарная операция, и состоит из чтения, сложения и записи. Подробнее в статье про AtomicInteger.
Пример есть на GitHub.