Что может заставить обновить энергонезависимую переменную?

Этот вопрос вдохновлен

Возьмите этот JVM

java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)

и следующий код

public class Driver implements Runnable {
    public static void main(String args[]) throws Exception {
        Driver driver = new Driver();
        Thread thread = new Thread(driver);
        thread.start();
        Thread.sleep(100); // make sure threads are unsynced 
        driver.stop = true;
    }

    boolean stop = false; // not volatile

    @Override
    public void run() {
        // Loop until stop is set to true.
        while (!stop) {
            doSomething(stop);
            // System.out.println("random string");
        }
        System.out.println("done");
    }

    public static void doSomething(boolean value) {
        /*ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            out.write(new byte[] {1});
        } catch (IOException e) {
            e.printStackTrace();
        }*/
    }
}

Когда я запускаю код, поток thread никогда не видит значение stop как true и поэтому никогда не выходит из цикла while (он может в конечном итоге, но в краткосрочной перспективе он не).

Обратите внимание на использование stop в этом вызове

doSomething(stop);

В байтовом коде это эквивалентно его использованию в цикле while, т.е. есть инструкция байтового кода

  11: getfield      #14                 // Field stop:Z

Поэтому я не понимаю, почему это должно иметь какой-то эффект.

Теперь, если вы раскомментируете один из

// System.out.println("random string");

или код внутри doSomething(boolean), тогда код выполняет около 100 мс (время Thread.sleep()), а затем заканчивается естественно, т.е. значение stop синхронизируется.

Ничто из того, что я знаю, не объясняет этого поведения. Похоже, использование OutputStream влияет на то, как и когда поток обновляет его представление о значении поля. Возможно ли это или я чего-то не хватает?

Что может заставить обновить представление потока значения энергонезависимой переменной?

Ответ 1

Ничто из того, что я знаю, не объясняет этого поведения. Кажется, что использование OutputStream влияет на то, как и когда поток обновляет его представление о значении поля.

System.out является PrintStream не an OutputStream. Все методы в PrintStream синхронизируются в потоке:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

Каждый раз, когда вы вводите блок synchronized, для этого потока есть событие синхронизации памяти чтения, которое вызывает обновление булевского поля.

Что может заставить обновить представление потока значения энергонезависимой переменной?

Поэтому ввод в блок synchronized приведет к этому. Также чтение из любой переменной volatile вызывает синхронизацию чтения. Поток также может быть выгружен из своего процессора, заставляя его перезагрузить его кэш-память, когда он получает следующий фрагмент времени.


Чтобы добавить более подробную информацию из обсуждения с помощью @Voo, это не рассматривается в JLS. Об этом говорится в документах JSR-133, в которых подробно описаны требования JMM. Чтобы процитировать из этого JSR-133 FAQ:

Но для синхронизации есть больше, чем взаимное исключение. Синхронизация гарантирует, что запись в памяти по потоку до или во время синхронизированного блока становится видимым предсказуемым образом для других потоков, которые синхронизируются на одном мониторе. После выхода из синхронизированного блока мы выходим из монитора, который приводит к сбросу кеша в основную память, так что записи, созданные этим потоком, могут быть видны другим потокам. Прежде чем мы сможем ввести синхронизированный блок, мы получим монитор, который имеет эффект недействительности кэша локального процессора, так что переменные будут перезагружены из основной памяти. Затем мы сможем увидеть все записи, сделанные видимыми предыдущей версией.

Таким образом, даже при работе с несинхронизированным, энергонезависимым полем, если поток вносит в него изменения, а затем записывается в поле volatile или оставляет блок synchronized, это поле будет опубликовано. Если после этого другой поток читается из любого поля volatile или входит в любой блок synchronized, тогда он увидит, что это поле обновлено.

Это не означает, что есть какие-либо гарантии заказа. Существует много путаницы вокруг порядка синхронизации, а не синхронизации памяти. Если два потока синхронизируются на двух разных объектах, то нет никаких гарантий вокруг порядка операций из-за гонки данных. Однако, если поле было обновлено потоком-A перед событием синхронизации памяти записи, оно будет опубликовано. И если поле было опубликовано другим потоком до того, как поток B имеет событие синхронизации памяти чтения, он будет обновлен.

Ответ 2

Здесь есть две вещи:

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

Чисто из JLS (точнее, JMM), который на самом деле недостаточно, чтобы убедиться, что вы видите обновление до stop, потому что в другом потоке никогда не был обнаружен тот же монитор. Но с чисто практической точки зрения выясняется, что многие CPU (и, конечно же, x86) не делают такого тщательного различия, поэтому вы увидите, что полная память обновлена, даже если спецификация не гарантирует. Чтобы получить гарантию по спецификации, что вы увидите обновление, поток, который устанавливает stop=true, затем будет иметь что-то само по себе и тем самым получить тот же замок. Затем он гарантировал, что в следующий раз, когда вы напечатаете что-то, вам нужно будет увидеть обновленное значение остановки.

Вероятно, вероятно, причина в том, что println является нативным методом и, вероятно, не встроен JVM, поэтому он не может выполнить анализ escape всех задействованных переменных и поэтому должен предположить, что println изменяет значение stop. Следовательно, он не может изменить

while (!stop) {
  doSomething(stop); // doSomething does not change value of stop variable
}

to

if (stop) return;
while (true) 
      doSomething(stop);  // doSomething does not change value of stop