У меня такой код:

 private volatile boolean run = true;

 private Object lock =new Object();

.........

Thread newThread = new Thread(new Runnable() {

    @Override
        public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {

            e.printStackTrace();
        }
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName()
                        + " run:" + run);

                System.out.println(Thread.currentThread().getName()
                        + " setting run to false");

                run = false;

                System.out.println(Thread.currentThread().getName()
                        + " run:" + run);
            }
        }});

newThread.start();

while(true) {//no synchronization, so no coordination guarantee
    System.out.println(Thread.currentThread().getName() + "* run: "+run);

    if(run == false) {
    System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting...");
    System.exit(0);
    }
}




which generates the following output:



main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
Thread-0 setting run to false
Thread-0 run:false
main* run: true    <- what causes this???
main** run: false
Exiting...

Я пытаюсь понять, почему в основном потоке возникает аберрация main * run: true , учитывая, что run является изменчивым полем и, согласно спецификации модели памяти Java, изменчивой записи в Thread-0 должен быть немедленно виден потоком main. Я знаю, что синхронизация в Thread-0 здесь неуместна, но меня смущает такое поведение изменчивого. Что мне здесь не хватает?

Другой, еще более странный результат дал следующее:

main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main** run: false
Exiting...
Thread-0 run:false

Или такого поведения следует ожидать, и если да, то почему? Спасибо.

Изменить: как просили в комментариях, я обновляю сообщение ожидаемым результатом, который я вижу иногда, но не все время:

main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
Thread-0 setting run to false
main* run: true
main* run: true
main* run: true
Thread-0 run:false
main** run: false
Exiting...

Другими словами, я не хочу видеть:

main* run: true 

Появиться после

Thread-0 run:false

Или

main** run: false
Exiting...

Предстать перед

Thread-0 run:false
1
Simeon Leyzerzon 4 Сен 2016 в 05:05

3 ответа

Лучший ответ

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

1 - main: read run (run is true)
2 - Thread-0: write run (run is false)
3 - main: read run (run is false)

Но выходные данные консоли - это отдельные действия, которые не обязательно должны выполняться сразу после чтения. Оценка параметров println и вызов метода не атомарны. Итак, у нас есть что-то вроде:

1 - main: read run (run is true)
2 - main: println("Run: true")

3 - Thread-0: write run (run is false)
4 - Thread-0: println("Run: false")

5 - main: read run (run is false)
6 - main: println("Run: false")

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

1 - main: read run (run is true)

3 - Thread-0: write run (run is false)
4 - Thread-0: println("Run: false")

2 - main: println("Run: true")

5 - main: read run (run is false)
6 - main: println("Run: false")

На основе исходного кода в PrintWriter, строка:

System.out.println(Thread.currentThread().getName() + " run:" + run);

Можно встроить что-то вроде:

String x = Thread.currentThread().getName() + " run:" + run;
synchronized(System.out.lock) {
    System.out.print(x);
    System.out.println();
}

Так что есть синхронизация внутри println, но она не включает чтение run. Это означает, что значение run может изменяться между чтением и выводом, в результате чего выводится старое значение run.

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

1
fgb 4 Сен 2016 в 15:54

Что касается правильности вашей программы, блок synchronized совершенно не нужен. Если вы когда-либо синхронизируете блокировку только из одного потока, есть большая вероятность, что JIT-компилятор устранит это заблокировать вообще.

Что действительно сбивает вас с толку, так это гарантия JMM, что поле volatile гарантированно будет видно с его обновленным значением другим потокам после того, как оно было записано. Однако эта гарантия не подразумевает, что поток, записывающий поле volatile, немедленно передает это значение и останавливается, пока новое значение не будет передано всем другим потокам. Вместо этого гарантируется, что другой поток в конечном итоге должен увидеть обновленное значение.

Это означает, что если поток A записывает в поле volatile, поток B гарантированно:

  1. В конце концов увидите это новое значение.
  2. Не считывать значения, записанные ранее в изменчивое поле с их «старым» значением.

Также обратите внимание, что вызов System.out.println неявно синхронизируется с объектом System.out (см. Код PrintWriter). Учитывая тот факт, что вы синхронизируете оба потока на одном мониторе, также объясняется наблюдаемый результат. Я предполагаю, что строка создается, когда монитор System.out заблокирован вашим потоком настройки поля. В этом случае поток сначала создает строку для записи, а затем ждет, пока другой поток освободит этот монитор, поэтому вы обычно наблюдаете вывод со «старым» содержимым.

Я имею в виду, что заявление

System.out.println(Thread.currentThread().getName() + "* run: " + run);

Не атомарен. Разделенное на два этапа утверждение эквивалентно следующему:

String text = Thread.currentThread().getName() + "* run: " + run;
System.out.println(text);

Учитывая эту неатомарность, цепочка событий (именование ваших потоков A и B ) выглядит так:

/*A*/ String text = Thread.currentThread().getName() + "* run: " + run;
/*B*/ System.out.println(Thread.currentThread().getName() + " setting run to false");
/*B*/ run = false;
/*B*/ System.out.println(Thread.currentThread().getName() + " run:" + run);
/*A*/ System.out.println(text);
/*A*/ if(run == false) {
/*A*/ System.out.println(Thread.currentThread().getName() + "** run: " + run + "\nExiting...");
/*A*/ System.exit(0);
/*A*/ }

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

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

1
Rafael Winterhalter 4 Сен 2016 в 15:11

Я не вижу проблемы. Замок здесь бесполезен. Также volatile означает, что переменная синхронизируется внутри самой себя. Вот что случилось. Когда есть несколько потоков, каждый запускается сам по себе, не заботясь о других. Итак, в этом случае у нас есть два потока: основной и поток-0. Main выполняется сам по себе и достигает точки, в которой он печатает переменную run, поэтому он печатает ее. Другой поток немного спит (что не имеет значения и не должно позволять другому потоку выполнять работу первым), а затем изменяет переменную run на false. Основной поток считывает новое значение и существует

Следуйте временной последовательности, и вы поймете

Thread newThread = new Thread(new Runnable() {

@Override
    public void run() {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {

        e.printStackTrace();
    }
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    + " run:" + run);

            System.out.println(Thread.currentThread().getName()
                    + " setting run to false");

            run = false; //<---- time_4

            System.out.println(Thread.currentThread().getName()
                    + " run:" + run); //<---- time_5
        }
    }});

newThread.start();

while(true) { //<---- time_2
    System.out.println(Thread.currentThread().getName() + "* run: "+run); //<--- time_3 getting the value of run variable. //<---- time_6 printing

    if(run == false) { //<---- time_1 (run == true) // <---- 2nd iteration time_7 (run == false)
    System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting..."); //<---- time_8
    System.exit(0);
    }
}

В любом случае, вот как исправить ваш код, чтобы получить ожидаемый результат (примечание: volatile здесь ничего не делал):

synchronized (lock) {
                if(run == false) {
                    System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting...");
                    System.exit(0);
                }
            }

Вот что volatile делает в основном для переменной run:

// run = false; //becomes ========
synchronized(someLock) {
    run = flase;
}
// =======================


//System.out.println(run); //becomes =========   
synchronized(someLock) {
    boolean tmpBoolean = run;
}
System.out.println(tmpBoolean);
//=================
2
Multithreader 4 Сен 2016 в 03:28