Как понять изменчивый пример в спецификации языка Java? - PullRequest
4 голосов
/ 24 мая 2019

Я думаю, что пример volatile в спецификации Java немного неправильный.

В 8.3.1.4. Летучие поля, это говорит

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

... тогда метод два мог бы иногда печатать значение для j, которое больше значения i, потому что пример не включает синхронизацию и, согласно правилам, объясненным в §17.4, общие значения i и j могут быть обновлен не по порядку.

Я думаю, что даже если эти обновления в порядке, во втором методе все равно можно увидеть j больше, чем i, поскольку System.out.println("i=" + i + " j=" + j) не является атомарным, и i читается до j.

Второй метод такой же, как

read i
read j

Так что возможно, что

read i
i++
j++
read j

В этом случае метод два видит значение для j, которое больше, чем i, однако обновления НЕ нарушены.

Так что не в порядке не единственная причина увидеть j> i

Должно ли это быть System.out.println("j=" + j + " i=" + i);?

На этот раз не по порядку единственная причина увидеть j> i

Ответы [ 2 ]

5 голосов
/ 24 мая 2019

Примеры более чем «немного ошибочны».

Во-первых, вы правы, что даже без переупорядочения j может выглядеть больше i в этом примере. Это даже подтверждается позже в того же примера :

Другим подходом было бы объявить i и j равными volatile:

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

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

Конечно, трудно сказать « общее значение для j никогда не больше, чем для i», просто сказать прямо в следующем предложении « Это возможно… [to] наблюдать значение для j, которое намного больше, чем значение, наблюдаемое для i ”.

Итак, j никогда не больше i, кроме случаев, когда наблюдается, что оно намного больше i? Предполагается ли сказать, что «немного больше» невозможно?

Конечно нет. Это утверждение не имеет смысла и, по-видимому, является результатом попытки отделить некоторую объективную истину, такую ​​как «общая ценность», от «наблюдаемой ценности», тогда как на самом деле в программе наблюдается только наблюдаемое поведение.

Это иллюстрируется неправильным предложением:

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

Даже с переменными volatile такой гарантии нет. Все, что JVM должна гарантировать, - это то, что наблюдаемое поведение не противоречит спецификации, поэтому, например, когда вы вызываете one() тысячу раз в цикле, оптимизатор все равно может заменить его атомарным увеличение на тысячу, если это может исключить возможность того, что другой поток станет свидетелем наличия такой оптимизации (кроме выведения из более высокой скорости).

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

Изменение порядка чтения i и j в пределах two() может сделать его лучшим примером, но я думаю, было бы лучше, если бы JLS §8.3.1.2 не пытался объяснить значение volatile в разговорной речи, но только что заявил, что он налагает особую семантику в соответствии с моделью памяти и оставил это JMM, чтобы объяснить это формально правильно.

Программисты не должны осваивать параллелизм, просто читая 8.3.1.4., Поэтому пример здесь не имеет смысла (в лучшем случае; в худшем случае создается впечатление, что этого примера достаточно для понимания вопроса).

0 голосов
/ 24 мая 2019

То, что Хольгер говорит в своем ответе, абсолютно правильно (прочитайте его снова и примите это), я просто хочу добавить, что с помощью jcstress это даже довольно легко доказать. Сам тест является лишь второстепенным рефактором из Coherence Sample (что превосходно! IMO):

import org.openjdk.jcstress.annotations.Actor;
import org.openjdk.jcstress.annotations.Expect;
import org.openjdk.jcstress.annotations.JCStressTest;
import org.openjdk.jcstress.annotations.Outcome;
import org.openjdk.jcstress.annotations.State;
import org.openjdk.jcstress.infra.results.II_Result;

@JCStressTest
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "only j updated")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "only i updated")
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "both updates lost")
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "both updated")
@State
public class SOExample {

    private final Holder h1 = new Holder();
    private final Holder h2 = h1;

    @Actor
    public void writeActor() {
        ++h1.i;
        ++h1.j;

    }

    @Actor
    public void readActor(II_Result result) {
        Holder h1 = this.h1;
        Holder h2 = this.h2;

        h1.trap = 0;
        h2.trap = 0;

        result.r1 = h1.i;
        result.r2 = h2.j;
    }

    static class Holder {

        int i = 0;
        int j = 0;

        int trap;
    }

}

Даже если вы не понимаете код, дело в том, что его запуск покажет ACCEPTABLE_INTERESTING как абсолютно возможные результаты; будь то с volatile int i = 0; volatile int j = 0; или без этого volatile.

...