Is id = 1 - id атомный?

Со страницы 291 экзамена на экзамен по программированию на Java SE 6, вопрос 25:

public class Stone implements Runnable {
    static int id = 1;

    public void run() {
        id = 1 - id;
        if (id == 0) 
            pick(); 
        else 
            release();
    }

    private static synchronized void pick() {
        System.out.print("P ");
        System.out.print("Q ");
    }

    private synchronized void release() {
        System.out.print("R ");
        System.out.print("S ");
    }

    public static void main(String[] args) {
        Stone st = new Stone();
        new Thread(st).start();
        new Thread(st).start();
    }
}

Один из ответов:

Выходной сигнал может быть P Q P Q

Я правильно ответил на этот ответ. Мои рассуждения:

  • Мы начинаем два потока.
  • Сначала вводится run().
  • В соответствии с JLS 15.26.1 он сначала оценивает 1 - id. Результат 0. Он хранится в стеке потоков. Мы собираемся сохранить это 0 в static id, но...
  • Бум, планировщик выбирает второй поток для запуска.
  • Итак, второй поток входит в run(). Статический id по-прежнему 1, поэтому он выполняет метод pick(). P Q.
  • Планировщик выбирает первый поток для запуска. Он берет 0 из своего стека и сохраняет до статического id. Итак, первый поток также выполняет pick() и печатает P Q.

Однако в книге написано, что этот ответ неверен:

Это неверно, потому что строка id = 1 - id меняет значение id между 0 и 1. Нет возможности для того, чтобы один и тот же метод выполнялся дважды.

Я не согласен. Я думаю, что есть шанс для сценария, который я представил выше. Такой обмен не является атомарным. Я не прав?

Ответ 1

Я не прав?

Нет, вы абсолютно правы - как ваша временная шкала.

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

Это несколько сбивает с толку, поскольку ссылочный материал как это неверен: (

Ответ 2

По моему мнению, ответ на Практических экзаменах правильный. В этом коде вы выполняете два потока, которые имеют доступ к одному и тому же идентификатору статической переменной. Статические переменные хранятся в куче в java, а не в стеке. Порядок выполнения runnables непредсказуем.

Однако, чтобы изменить значение id каждого потока:

  • делает локальную копию значения, хранящегося в адресе памяти id, в реестр CPU;
  • выполняет операцию 1 - id. Строго говоря, здесь выполняются две операции (-id and +1);
  • возвращает результат обратно в область памяти id в куче.

Это означает, что хотя значение id может быть изменено одновременно одним из двух потоков, только начальное и конечное значения изменяемы. Промежуточные значения не будут изменены друг другом.

Более того, анализ кода может показать, что в любой момент времени id может быть только 0 или 1.

Доказательство:

  • Начальное значение id = 1; Один поток изменит его на 0 (id = 1 - id). И другой поток вернет его к 1.

  • Начальное значение id = 0; Один поток изменит его на 1 (id = 1 - id). И другой поток вернет его к 0.

Следовательно, состояние значения id дискретно либо 0, либо 1.

Конец доказательства.

Для этого кода могут быть две возможности:

  • Возможность 1. В потоке один сначала обращается к идентификатору переменной. Тогда значение id (id = 1 - id изменится на 0. После этого будет выполнен только метод pick (), напечатав P Q. Поток второй, будет оценивать id в это время id = 0, тогда метод release() будет выполненная печать R S. В результате будет напечатано P Q R S.

  • Возможность 2. В потоке два сначала обращаются к идентификатору переменной. Тогда значение id (id = 1 - id изменится на 0. После этого будет выполнен только метод pick (), напечатав P Q. В первом случае будет оцениваться идентификатор в это время id = 0; тогда метод release() будет выполненная печать R S. В результате будет напечатано P Q R S.

Других возможностей нет. Однако следует отметить, что варианты P Q R S, такие как P R Q S или R P Q S и т.д., Могут быть напечатаны из-за того, что pick() является статическим методом и поэтому разделяется между двумя потоками. Это приводит к одновременному выполнению этого метода, что может привести к печати букв в другом порядке в зависимости от вашей платформы.

Однако в любом случае никогда не будет выполняться два метода pick() или release (), поскольку они взаимоисключающие. Поэтому P Q P Q не будет выводиться.