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 отличается?




Все зависит от порядка выполнения. 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 также может изменить некоторый порядок (хотя гарантия «происходит раньше» по-прежнему сохраняется).
Огромное спасибо за помощь. Этот вопрос меня беспокоит уже давно, еще раз спасибо!
У вас состояние гонки. System.out.println достаточно медленный, поэтому m1 не может переназначить b до того, как основной поток распечатает b. Вполне возможно, хотя и очень маловероятно, что m1 запустится раньше m2. Возможные результаты: ("----", "b = 1000", "2000"), ("1000", "----", "b = 1000"), ("2000", "-- --", "б = 1000"). Однако последний вариант вы видите довольно постоянно, потому что запуск потока медленнее, чем просто вызов метода, следовательно, m2 раньше m1.