Неустойчивое переупорядочивание Java

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

Я новичок в параллелизме, и мне дали следующий код, чтобы ответить на вопросы:

  • а) Почему возможен любой другой вывод, кроме 00?
  • б) Как изменить код, чтобы 00 печаталось ВСЕГДА.
 boolean flag = false;

    void changeVal(int val) {
        if(this.flag){
            return;
        }
        this.initialInt = val;
        this.flag = true;
    }

    int initialInt = 1;

    class MyThread extends Thread {
        public void run(){
            changeVal(0);
            System.out.print(initialInt);
        }
    }

    void execute() throws Exception{
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start(); t2.start(); t1.join(); t2.join();
        System.out.println();
    }

Для a) мой ответ будет следующим: в отсутствие какой-либо конструкции volatile/sync компилятор может изменить порядок некоторых инструкций. В частности, this.initialInt = val; и this.flag = true; может быть переключен так, чтобы могла возникнуть следующая ситуация: оба потока запущены, и t1 загружается вперед. Учитывая переупорядоченные инструкции, он сначала устанавливает флаг = true. Теперь, прежде чем он достигнет теперь последнего оператора this.initialInt = val; другой поток прыгает, проверяет условие if и немедленно возвращает, таким образом печатая неизменное значение initialInt, равное 1. Кроме того, я считаю, что без какой-либо volatile/синхронизации неясно, может ли t2 увидеть назначение, выполненное для initialInt в t1 поэтому он также может печатать 1 как значение по умолчанию.

Для b) я думаю, что этот флаг можно сделать изменчивым. Я узнал, что когда t1 записывает в изменчивую переменную, устанавливающую флаг = true, тогда t2, после считывания этой изменчивой переменной в операторе if, увидит любые операции записи, выполненные до изменчивой записи, следовательно, initialInt = val тоже. Следовательно, t2 уже увидит, как его значение initialInt изменилось на 0, и всегда должен печатать 0. Однако это будет работать только в том случае, если использование volatile успешно предотвращает любое изменение порядка, как я описал в а). Я читал о том, что volatile выполняет такие вещи, но я не уверен, всегда ли это работает здесь при отсутствии каких-либо дополнительных синхронизированных блоков или каких-либо подобных блокировок. Из этот ответ я понял, что ничего, что происходит до энергозависимого хранилища (так что this.flag = true), не может быть переупорядочено так, чтобы оно отображалось за пределами Это. В этом случае initialInt = val не может быть перемещен вниз, и я должен быть прав, верно? Или не ? :)

Большое вам спасибо за вашу помощь. Я с нетерпением жду ваших ответов.


person Dbadtf_385    schedule 29.07.2020    source источник


Ответы (2)


Этот пример всегда будет печатать 00 , потому что вы делаете changeVal(0) перед печатью .

чтобы имитировать случай, когда 00 не может быть напечатано, вам нужно переместить initialInt = 1; в контекст потока следующим образом:

class MyThread extends Thread {
        public void run(){
            initialInt = 1;
            changeVal(0);
            System.out.print(initialInt);
        }
    }

теперь у вас может быть состояние гонки, которое возвращает initialInt к 1 в потоке 1, прежде чем оно будет напечатано в потоке 2.

другая альтернатива, которая может привести к состоянию гонки, но которую сложнее понять, - это переключение порядка установки флага и установки значения.

void changeVal(int val) {
        if(this.flag){
            return;
        }
        this.flag = true;
        this.initialInt = val;
 }
person Shachaf.Gortler    schedule 29.07.2020
comment
Спасибо. Но будет ли этот переключатель появляться только тогда, когда он явно закодирован, или он также может быть результатом переупорядочения выражений компилятором в отсутствие функций синхронизации, таких как volatile. Затем возникает вопрос, будет ли использование переменной флага volatile полностью исключать возможность того, что (^) это переупорядочение произойдет, тем не менее - person Dbadtf_385; 30.07.2020
comment
Нет, чтобы полностью синхронизировать поток расширяемым способом, вам необходимо использовать конструкции высокого уровня, такие как критический раздел, мьютекс и т.д. volatile может работать в этом простом случае, когда у вас есть только 1 общая переменная. - person Shachaf.Gortler; 30.07.2020

Нет явной синхронизации, поэтому возможны все виды чередования, и изменения, сделанные одним потоком, не обязательно видны другому, поэтому возможно, что изменения в flag видны до изменений в initialInt, вызывая вывод 10 или 01 , а также вывод 00. 11 невозможно, потому что операции, выполняемые над переменными, видны выполняющему их потоку, а эффекты changeVal(0) всегда будут видны хотя бы для одного из потоков.

Если сделать changeVal синхронизированным или flag изменчивым, проблема будет устранена. flag — это последняя переменная, измененная в критической секции, поэтому объявление ее как volatile создаст отношение «произошло до», делая изменения в initialInt видимыми.

person Burak Serdar    schedule 29.07.2020
comment
Спасибо за ответ. Итак, я прав, предполагая, что volatile действительно исключает возможность переупорядочения между this.initialInt = val (давайте назовем это присваивание x); а this.flag = true(y);? Этот переключатель был бы возможен в отсутствие volatile (помимо других упомянутых вами чередований), верно? Итак, просто для уточнения: если бы я добавил дополнительное присваивание z перед x даже с volatile-флагом, компилятор все равно мог бы произвольно переключать z и x, однако он не мог бы перемещать ни z, ни x дальше y, это правда? - person Dbadtf_385; 30.07.2020
comment
Это хорошая информация о volatile и должна объяснить, как все работает: tutorials.jenkov.com/java -concurrency/volatile.html - person Burak Serdar; 30.07.2020