Java: `volatile` частные поля с геттерами и сеттерами

Должны ли мы объявить частные поля как volatile, если instanced используются в нескольких потоках?

В Эффективная Java есть пример, где код не работает без изменчивости:

import java.util.concurrent.TimeUnit;

// Broken! - How long would you expect this program to run?
public class StopThread {
    private static boolean stopRequested; // works, if volatile is here

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested)
                    i++;
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

В объяснениях говорится, что

while(!stopRequested)
    i++;

оптимизирован примерно так:

if(!stopRequested)
    while(true)
        i++;

поэтому дальнейшие модификации stopRequested не видны фоновым потоком, поэтому он навсегда зацикливается. (BTW, этот код заканчивается без volatile на JRE7.)

Теперь рассмотрим этот класс:

public class Bean {
    private boolean field = true;

    public boolean getField() {
        return field;
    }

    public void setField(boolean value) {
        field = value;
    }
}

и поток следующим образом:

public class Worker implements Runnable {
    private Bean b;

    public Worker(Bean b) {
        this.b = b;
    }

    @Override
    public void run() {
        while(b.getField()) {
            System.err.println("Waiting...");
            try { Thread.sleep(1000); }
            catch(InterruptedException ie) { return; }
        }
    }
}

Вышеприведенный код работает, как ожидается, без использования летучих:

public class VolatileTest {
    public static void main(String [] args) throws Exception {
        Bean b = new Bean();

        Thread t = new Thread(new Worker(b));
        t.start();
        Thread.sleep(3000);

        b.setField(false); // stops the child thread
        System.err.println("Waiting the child thread to quit");
        t.join();
        // if the code gets, here the child thread is stopped
        // and it really gets, with JRE7, 6 with -server, -client
    }
}

Я думаю, что из-за публичного сеттера компилятор /JVM никогда не должен оптимизировать код, который вызывает getField(), но в этой статье говорит, что есть некоторые "летучие Bean" (шаблон № 4), который должен применяться для создания изменяемых потокобезопасных классов. Обновление: Возможно, эта статья применяется только для IBM JVM?

Вопрос: какая часть JLS явно или неявно говорит, что частные примитивные поля с общедоступными getters/setters должны быть объявлены как volatile (или им не обязательно)?

Извините за длинный вопрос, я попытался подробно объяснить проблему. Дайте мне знать, если что-то неясно. Спасибо.

Ответы

Ответ 1

Прежде чем ответить на ваш вопрос, я хочу обратиться к

Кстати, этот код заканчивается без изменчивости на JRE7

Это может измениться, если вы хотите развернуть одно и то же приложение с разными аргументами времени исполнения. Подъем не обязательно является реализацией по умолчанию для JVM, поэтому он может работать в одном, а не в другом.

Чтобы ответить на ваш вопрос, нет ничего, что помешало бы компилятору Java выполнить ваш последний пример так

@Override
public void run() {
    if(b.getField()){
        while(true) {
            System.err.println("Waiting...");
            try { Thread.sleep(1000); }
            catch(InterruptedException ie) { return; }
        }
    }
}

Он по-прежнему последовательно согласован и, таким образом, поддерживает Java-гарантии - вы можете читать специально 17.4.3:

Среди всех действий между потоками, выполняемых каждым потоком t, программный порядок t - это полный порядок, который отражает порядок, в котором эти действия будут выполняться в соответствии с внутрипоточными семантика t.

Набор действий последовательно согласован, если все действия происходят в общий порядок (порядок выполнения), соответствующий программе порядок, и, кроме того, каждый считываемый r переменной v видит значение записанный путем записи w в v такой, что:

Иными словами - до тех пор, пока поток увидит чтение и запись поля в том же порядке, независимо от порядка упорядочивания компилятора/памяти, считается последовательным.

Ответ 2

Вопрос: какая часть JLS явно или неявно говорит, что частные примитивные поля с общедоступными getters/seters должны быть объявлены как изменчивые (или им не обязательно)?

Модель памяти JLS не заботится о геттерах/сеттерах. Они не являются операционными системами с точки зрения модели памяти - вы также можете получать доступ к публичным полям. Обтекание логической точки за вызовом метода не влияет на видимость его памяти. Ваш последний пример работает исключительно удачей.

Должны ли мы объявлять частные поля как volatile, если инстанс используется в нескольких потоках?

Если класс (bean) должен использоваться в многопоточной среде, вы должны как-то учитывать это. Создание частных полей volatile - это один из подходов: он гарантирует, что каждому потоку гарантировано будет отображаться последнее значение этого поля, а не что-либо кэшированное/оптимизированное от устаревших значений. Но это не решает проблему atomicity.

Статья, к которой вы привязаны, относится к любой JVM, которая придерживается спецификации JVM (на которую опирается JLS). Вы получите различные результаты в зависимости от поставщика JVM, версии, флагов, компьютера и ОС, количества раз, когда вы запускаете программу (оптимизация HotSpot часто срабатывает после 10000-го запуска) и т.д., Поэтому вы действительно должны понимать спецификацию и тщательно придерживаться к правилам для создания надежных программ. Экспериментировать в этом случае - это плохой способ узнать, как все работает, потому что JVM может вести себя так, как хочет, так долго, пока он попадает в спецификацию, и большинство JVM действительно содержат нагрузки всех видов динамических оптимизаций.

Ответ 3

Нет, этот код так же неверен. Ничто в JLS не говорит, что поле должно быть объявлено изменчивым. Однако, если вы хотите, чтобы ваш код работал правильно в многопоточной среде, вы должны соблюдать правила видимости. энергозависимые и синхронизированные - это два из основных средств для правильного отображения данных по потокам.

Что касается вашего примера, то сложность написания многопоточного кода заключается в том, что многие формы неправильного кода отлично работают при тестировании. Просто потому, что многопоточный тест "преуспевает" при тестировании не означает, что он правильный код.

Для конкретной ссылки JLS см. раздел Happens Before (и остальная часть страницы).

Обратите внимание, что как общее правило, если вы думаете, что у вас появился умный новый способ обойти "стандартные" потокобезопасные идиомы, вы, скорее всего, ошибаетесь.