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

Изменение переменной, сделанное в одном потоке, не сразу видно другому потоку. Исправить это можно с помощью 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.

 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *