Java. Ключевое слово volatile

Java. Ключевое слово volatile

t.me/javatg

Что такое 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

Источник



Report Page