Путаница с синхронизацией потоков

public class Interview implements Runnable {
    int b = 100;
    public synchronized void m1() throws Exception {
        //System.out.println("-----");
        b = 1000;
        Thread.sleep(5000);
        System.out.println("b = " + b);
    }

    public synchronized void m2() throws Exception {
        Thread.sleep(2500);
        b = 2000;
    }

    @Override
    public void run() {
        try {
            m1();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws Exception {
        Interview interview = new Interview();
        Thread thread = new Thread(interview);
        thread.start();
        interview.m2();
        System.out.println(interview.b);
    }
}

Когда я закомментирую строку "System.out.println(" --");", результат будет такой:

1000  
b = 1000   

Когда я не комментирую строку "System.out.println(" --");" Результат:

2000   
-----    
b = 1000    

Почему значение interview.b отличается?

У вас состояние гонки. System.out.println достаточно медленный, поэтому m1 не может переназначить b до того, как основной поток распечатает b. Вполне возможно, хотя и очень маловероятно, что m1 запустится раньше m2. Возможные результаты: ("----", "b = 1000", "2000"), ("1000", "----", "b = 1000"), ("2000", "-- --", "б = 1000"). Однако последний вариант вы видите довольно постоянно, потому что запуск потока медленнее, чем просто вызов метода, следовательно, m2 раньше m1.

matt 05.06.2024 11:49
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
1
85
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Все зависит от порядка выполнения. m1() и m2() могут идти только друг за другом, но System.out.println(interview.b); может идти параллельно m1().

Хотя это не может быть гарантировано, вполне вероятно, что m2() запустится до того, как другой поток раскрутится и запустится m1(), поэтому b будет иметь значение 2000 непосредственно перед тем, как выполнение достигнет System.out.println(interview.b);.
(Почему m2() скорее всего запустится раньше m1()? Потому что создание контекста потока занимает некоторое время, в течение которого m2(), возможно, уже получил блокировку синхронизации, поэтому m1() придется ждать. Добавьте Thread.sleep(1) перед вызовом m2(), и m1(), скорее всего, запустится первым).

И вот тут начинается самое интересное: m1(), скорее всего, продолжится, когда m2() снимет блокировку синхронизации и будет работать параллельно с System.out.println(interview.b);. Без System.out.println("-----"); первое, что m1() сделает, это установит b на 1000, что может произойти раньше System.out.println(interview.b);, поэтому вы увидите напечатанное 1000 вместо 2000. Но если вы добавите оператор печати, то b = 1000 задержится и, таким образом, System.out.println(interview.b); все равно напечатает 2000.

Позвольте мне попытаться проиллюстрировать.

Без System.out.println("-----");:

                        start sub thread
                        |  m2() acquires lock
                        | /     after sleeping b=2000 and then m2() releases the lock
                   main //     /  print(b) -> 1000
                   |   //     /  /
Main thread:   o---+---++-----+--+-x
Sub thread:            \++-----++----+-x
                        ||     ||     \
                       / |     |\      print("b = " + b) ->b=1000
  create thread context  |     | b=1000
  m1() starts but has to wait  \m1() acquires lock

С System.out.println("-----");:

                        start sub thread
                        |  m2() acquires lock
                        | /     after sleeping b=2000 and then m2() releases the lock
                   main //     /  print(b) -> 2000
                   |   //     /  /
Main thread:   o---+---++-----+--+-x
Sub thread:            \++-----++-+----+-x
                        ||     || |     \
                       / |     ||  \      print("b = " + b) ->b=1000
  create thread context  |     | \  b=1000
                         |     |  print("----")
  m1() starts but has to wait  \m1() acquires lock

Это может показаться немного расплывчатым со всеми этими «вероятно», «может» и т. д., и это проблема с параллелизмом/многопоточностью: параллельное выполнение может привести к различному воспринимаемому порядку операций (теоретически они могут произойти одновременно, но, конечно, на самом деле это не так — один может быть быстрее другого), а оптимизация JVM также может изменить некоторый порядок (хотя гарантия «происходит раньше» по-прежнему сохраняется).

Огромное спасибо за помощь. Этот вопрос меня беспокоит уже давно, еще раз спасибо!

苏苏沐哲 05.06.2024 11:51

Другие вопросы по теме