Не могу понять пример волатильности в спецификации Java

Я понял общее, что означает volatile в Java. Но чтение Спецификация Java SE 8.3.1.4 У меня есть проблема с пониманием текста под этим нестабильным примером.

class Test {
    static volatile int i = 0, j = 0;
    static void one() { i++; j++; }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

Это позволяет использовать метод один и два метода одновременно, но гарантирует, что доступ к общим значениям для я и j происходит точно столько же раз и в точно таком же порядке, как они появляются для выполнения во время выполнения текста программы каждым потоком. Поэтому общее значение для j никогда не больше, чем для i, потому что каждое обновление я должно быть отражено в общем значении для я до появления обновления до j. Возможно, однако, что любой данный вызов метода 2 может наблюдать значение j, которое много больше, чем значение, наблюдаемое для i, поскольку метод может быть выполняется много раз между моментом, когда метод два выбирает значение я и момент, когда метод два выбирает значение j.

Как

j никогда не больше i

но в то же время

любое заданное вызов метода два может наблюдать значение для j, которое намного больше, чем величина, наблюдаемая при i

??

Похоже на противоречие.

Я получил j больше, чем i после запуска программы-примера. Зачем использовать volatile? Он дает почти такой же результат без volatile (также i может быть больше, чем j, один из предыдущих примеров в спецификациях). Почему этот пример здесь является альтернативой synchronized?

Ответ 1

Я думаю, что в этом примере подчеркивается, что вам нужно позаботиться и обеспечить порядок при использовании volatile; поведение может быть противоречивым, и пример демонстрирует его.

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

Общее значение - это значение в тот же момент. Если два потока считывают значения я и j в точно такой же момент, значение j никогда не будет наблюдаться больше i. волатильные гарантии поддержания порядка чтения и обновления, как в коде.

Однако в образце print + i и + j - две разные операции, разделенные произвольным количеством времени; следовательно, j можно наблюдать больше, чем i, так как он может быть обновлен произвольным числом раз после чтения я и перед чтением j.

Точка использования volatile заключается в том, что при одновременном обновлении и доступе к изменчивым переменным с правильным порядком вы можете делать допущения, которые невозможны в принципе без летучих.

В приведенном выше примере порядок доступа в two() не позволяет заключить с уверенностью, какая переменная больше или равна.

Рассмотрим, однако, если образец был изменен на System.out.println("j=" + j + " i=" + i);

Здесь вы можете утверждать с уверенностью, что напечатанное значение j никогда не превышает печатное значение i. Это предположение будет не удерживаться без volatile по двум причинам.

Во-первых, обновления я ++ и j ++ могут выполняться компилятором и оборудованием в произвольном порядке и в действительности могут выполняться как j ++; я ++. Если из другого потока вы получаете доступ j и я после j++, но до i++ вы можете наблюдать, скажем, j=1 и i=0, независимо от порядка доступа. volatile гарантирует, что этого не произойдет, и он выполнит операции в том порядке, который написан в вашем источнике.

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

В то время как вторая гарантия может показаться следствием первой (гарантия заказа), они на самом деле ортогональны.

Что касается synchronized, он позволяет выполнять последовательность неатомных операций, например i++;j++ в качестве атомной операции, например. если один поток синхронизирован i++;j++, а другой синхронизирован System.out.println("i=" + i + " j=" + j);, первый поток может не выполнять последовательность инкремента, тогда как вторая печать и результат будут правильными.

Но это стоит дорого. Во-первых, synhronized имеет наказание за производительность. Во-вторых, что более важно, не всегда такое поведение требуется, и заблокированный поток тратит время, уменьшая пропускную способность системы (например, вы можете сделать так много я ++; j ++; во время System.out).

Ответ 2

В любой момент времени j не больше i.

Это отличается от того, какой метод наблюдает два, потому что он обращается к переменным i и j в разное время. i открывается, а затем j открывается чуть позже.

Это не является прямой альтернативой синхронизированной версии, поскольку поведение отличается. Одно отличие от использования неустойчивого заключается в том, что без летучих значений всегда можно печатать значения 0. Приращение никогда не должно быть видимым.

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

volatile boolean flag = false;
volatile int value;

// Thread 1
if(!flag) {
    value = ...;
    flag = true;
}

// Thread 2
if(flag) {
    System.out.println(value);
    flag = false;
}

и поток 2 считывает значение, указанное в потоке 1, а не старое значение.

Ответ 3

Я хотел бы предположить, что это ошибка, и примеры должны были печатать j до i:

static void two() {
    System.out.println("j=" + j + " i=" + i);
}

Новизна в первом примере заключается в том, что из-за переупорядочения обновлений j может быть больше, чем i, даже если это наблюдается в первую очередь.

Последний пример теперь имеет смысл с некоторыми незначительными изменениями в объяснении (правки и комментарии в скобках):

Это позволяет одновременно выполнять метод one и метод two, но гарантирует, что доступ к общим значениям для i и j происходит ровно столько раз, и точно в том же порядке, что и они появляются во время выполнения текста программы по каждому потоку. Поэтому общее значение для j никогда не будет [наблюдаемым] больше, чем для i, потому что каждое обновление до i должно отражаться в общем значении для i до того, как произойдет обновление до j, Возможно, однако, что любое заданное обращение метода two может наблюдать значение для [i], которое намного больше значения, наблюдаемого для [j], так как метод one может выполняться много раз между моментом, когда метод two извлекает значение [j] и момент, когда метод two извлекает значение [i].

Ключевым моментом здесь является то, что второе обновление никогда не будет наблюдаться до первого обновления при использовании volatile. Последнее предложение о разрыве между этими двумя чтениями полностью в скобках, и i и j были заменены, чтобы соответствовать ошибочному примеру.

Ответ 4

Как j никогда не больше i?

Скажем, вы выполняете one() только один раз. Во время выполнения этого метода я всегда увеличивается до j, так как операции приращения происходят один за другим. Если вы выполняете one() одновременно, каждый отдельный вызов метода будет ждать, пока другие методы в очереди выполнения завершат запись своих значений в я или j, в зависимости от того, какая переменная, которую пытается выполнить текущий исполняемый метод. Итак, все пишут, чтобы я происходил один за другим, и все записи в j происходят один за другим. И так как внутри самого тела метода я увеличивается до j, в данный момент j никогда не будет больше i.

любое заданное обращение метода два может наблюдать значение j, которое намного больше значения, наблюдаемого для i, как?

Если метод one() выполняется в фоновом режиме, когда вы вызываете two(), между чтением i и чтением j, метод может выполняться несколько раз. Итак, когда считывается значение i, это может быть результатом некоторого вызова one(), который произошел в момент времени t = 0, и когда считывается значение j, это может быть результатом вызова one(), что произошло позже во времени, например, при t = 10. Следовательно, j может быть больше i в этом случае в инструкции println.

Зачем использовать volatile вместо синхронизированного?

Я не буду перечислять все причины, по которым кто-то должен использовать volatile вместо блока synchronized. Но имейте в виду, что volatile гарантирует атомный доступ только к этому конкретному полю и не обеспечивает атомное выполнение блока кода, который не помечен как synchronized. В этом примере доступ к я и j синхронизирован, но общая операция {i ++; j ++} не синхронизирована, следовательно, очевидно (я использую, по-видимому, так как это не совсем то же самое, но выглядит аналогичным) дает те же результаты, что и без использования ключевое слово volatile.

Ответ 5

Как

j никогда не больше i

но в то же время

любой заданный вызов метода два может наблюдать значение j, которое намного больше, чем значение, наблюдаемое для i

??

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

Когда записывается переменная volatile, она записывает как ее, так и все, прежде чем она станет видимой для других потоков (по крайней мере, в Java 5+). Объяснение мало что изменило для версий Java до этого, хотя). Таким образом, приращение i должно быть видимым к моменту увеличения j, что означает, что j никогда не может быть больше, чем i для других потоков.

Чтения i и j, однако, не гарантируются в один момент выполнения программы. Чтение i и j может показаться очень близким к потоку, выполняющемуся two(), но на самом деле через чтение может пройти какое-то произвольное количество времени. Например, two() может читать i при i = 5 и j = 5, но затем "замораживаться", пока выполняются другие потоки, изменяя значения i и j, скажем, 20 и 19, соответственно. Когда two() возобновляется, он выбирает, где он остановился, и читает j, который теперь имеет значение 19. two() не перечитывает i, потому что, поскольку это касается, не было никакого прерывания в исполнении, поэтому нет необходимости проходить дополнительную работу.

Зачем использовать volatile?

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

Почему этот пример здесь является альтернативой синхронизации?

volatile является жизнеспособной альтернативой synchronized, только если one() выполняется одним потоком, что здесь и есть. В этом случае только один поток когда-либо пишет на i и j, поэтому нет необходимости в гарантиях на атомарность synchronized. Если one() выполнялся несколькими потоками, volatile не работал бы, потому что операции чтения-добавления-хранилища, которые составляют инкремент, должны происходить атомарно, а volatile не гарантирует этого.

Ответ 6

Эта программа гарантирует, что метод two() замечает j >= i-1 (не считая переполнения).

Без volatile наблюдаемые значения i,j могут быть повсюду.

Утверждение

общее значение для j никогда не больше, чем для i

является очень неформальным, потому что это означает "в то же время", что не является определенной концепцией в JMM.


Основным принципом JMM является "последовательная согласованность". Вождение мотивации JMM

JLS # 17 - Если программа правильно синхронизирована, все исполнения программы будут последовательно последовательными

В следующей программе

void f()
{
    int x=0, y=0;
    x++;
    print( x>y );
    y++
}

x>y всегда будет наблюдаться как true. Это должно быть, если мы будем следовать последовательности действий. В противном случае нам действительно не нужно рассуждать о каком-либо (императивном) коде. Это "последовательная согласованность".

"Последовательная согласованность" является наблюдаемым свойством, она не должна совпадать с "реальными" действиями (что бы это ни значило). Вполне возможно, что x>y оценивается как true с помощью JVM до того, как x будет фактически увеличиваться (или вообще). Пока JVM может гарантировать наблюдаемую последовательную согласованность, он может оптимизировать фактическое выполнение в любом случае, например, выполнить код не в порядке.

Но это для синглетной нити. Если несколько потоков являются чтением/записью общих переменных, такая оптимизация, конечно, полностью разрушит последовательную согласованность. Мы не можем рассуждать о поведении программы, думая о чередовании действий из нескольких потоков (с действиями в одном потоке после последовательности внутрипотока).

Если мы хотим гарантировать межпоточную последовательную согласованность любого многопоточного кода, мы должны отказаться от методов оптимизации, разработанных для одного потока. Это будет иметь серьезное снижение производительности для большинства программ. И это также невостребовано для - обмен данными между потоками довольно редок.

Поэтому специальные инструкции создаются только для установления последовательной согласованности между потоками, когда это необходимо. Неустойчивые чтения и записи являются такими действиями. Все изменчивые чтения и записи подчиняются последовательному согласованию между потоками. В этом случае он гарантирует, что j >= i-1 в two().

Ответ 7

Все зависит от того, как вы его используете. Ключевое слово volatile в Java используется как индикатор для компилятора Java и Thread, которые не кэшируют значение этой переменной и всегда читают ее из основной памяти. Поэтому, если вы хотите поделиться любой переменной, в которой операция чтения и записи является атомарной путем реализации, например. читать и писать в int или логической переменной, тогда вы можете объявить их как изменчивую переменную.

Из Java 5 наряду с основными изменениями, такими как аргументы Autoboxing, Enum, Generics и Variable, Java вводит некоторые изменения в Java Memory Model (JMM), что гарантирует видимость изменений, сделанных из одного потока в другой, также как "случится раньше", который решает проблему записи в памяти, которая происходит в одном потоке, может "просачиваться" и видна другим потоком.

Ключевое слово Java volatile не может использоваться с методом или классом, и его можно использовать только с переменной. Ключевое слово Java volatile также гарантирует видимость и порядок, после того, как Java 5 записывает любую изменчивую переменную, происходит до того, как будет прочитано изменчивая переменная. Кстати, использование ключевого слова volatile также предотвращает компилятор или JVM от переупорядочения кода или отключение их от барьера синхронизации.

Важные моменты ключевого слова Volatile в Java

  • Ключевое слово volatile в Java - это только приложение к переменной и использование ключевого слова volatile с классом и методом является незаконным.

  • ключевое слово volatile в Java гарантирует, что значение переменной volatile всегда будет считаться из основной памяти, а не из локального кэша Thread.

  • В Java чтение и запись являются атомарными для всех переменных, объявленных с использованием ключевого слова Java volatile (включая длинные и двойные переменные).

  • Использование ключевого слова volatile в Java по переменным уменьшает риск ошибок согласованности памяти, поскольку любая запись в переменную volatile в Java устанавливает взаимосвязь между событиями и последующими чтениями этой же переменной.