Ключевое слово volatile в Java
Что такое volatile.
Изменение переменной, сделанное в одном потоке, не сразу видно другому потоку. Исправить это можно с помощью volatile — ключевого слова, которое ставится перед переменной. В отличие от слова synchronized, которое применимо для метода или для блока кода, слово volatile применимо только для переменной. volatile — это более слабый вариант синхронизации, который иногда бывает достаточным.
Рассмотрим пример, показывающий, что изменение переменной в одном потоке действительно не сразу видно другому потоку (или даже никогда не видно).
volatile – этот модификатор вынуждает потоки отключить оптимизацию доступа и использовать единственный экземпляр переменной. Если переменная примитивного типа – этого будет достаточно для обеспечения потокобезопасности. Если же переменная является ссылкой на объект – синхронизировано будет исключительно значение этой ссылки. Все же данные, содержащиеся в объекте, синхронизированы не будут!
synchronized – это зарезервированное слово позволяет добиваться синхронизации в помеченных им методах или блоках кода.
Ключевые слова transient и native к многопоточности никакого отношения не имеют, первое используется для указания полей класса, которые не нужно сериализовать, а второе – сигнализирует о том, что метод реализован в платформо-зависимом коде.
@javatg – лучшие практики Java в нашем телеграм канале.
Некорректный код
Пусть поток 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.
@java_library – бесплатные книги Java