В посте "Помогите разобрать пример программы с многопоточностью и synchronized" камрад @Anonymous #2481584 попросил помочь разобраться что происходит в следующем коде:
package testpack;
class Deadlock implements Runnable {
class A {
synchronized void foo(B b) {
String name = Thread.currentThread().getName();
System.out.println(name + " entered A.foo");
try {
Thread.sleep(1000);
} catch(Exception e) {
System.out.println("A Interrupted");
}
System.out.println(name + " trying to call B.last()");
b.last();
}
synchronized void last() {
System.out.println("Inside A.last");
}
}
class B {
synchronized void bar(A a) {
String name = Thread.currentThread().getName();
System.out.println(name + " entered B.bar");
try {
Thread.sleep(1000);
} catch(Exception e) {
System.out.println("B Interrupted");
}
System.out.println(name + " trying to call A.last()");
a.last();
}
synchronized void last() {
System.out.println("Inside A.last");
}
}
A a = new A();
B b = new B();
Deadlock() {
Thread.currentThread().setName("MainThread");
Thread t = new Thread(this, "RacingThread");
t.start();
a.foo(b); // get lock on a in this thread.
System.out.println("Back in main thread");
}
public void run() {
b.bar(a); // get lock on b in other thread.
System.out.println("Back in other thread");
}
public static void main(String args[]) {
new Deadlock();
}
}
Мой ответ получился довольно многословным, поэтому я решил оформить его в виде небольшой статьи. Возможно данная публикация окажется интересна кому-нибудь ещё, кто недавно впервые столкнулся с многопоточностью.
Итак, давайте попытаемся понять, что же происходит в приведённом коде, когда возникает взаимная блокировка потоков. Так называемый "DeadLock":- В первом потоке, создаётся новый объект класса
DeadLock. В ходе работы конструктора, он создаёт второй поток, передав создаваемому потоку ссылку на конструируемый объект. - Затем, в первом потоке, вызывается метод запуска второго потока. Второй поток начинает исполнять метод
run(). Начинается гонка. - В первом потоке управление передаётся методу
foo()объектаa. Этот метод автоматически захватывает монитор объектаa, а затем в ходе выполнения 12 строки переводит первый поток в спящее состояние на одну секунду. Монитор объектаaвсё ещё захвачен. - В это время, второй поток передаёт управление методу
bar()объектаb. Монитор объектаbавтоматически захватывается. В ходе выполнения 32 строки, второй поток засыпает на одну секунду. Монитор объектаbостаётся захвачен. - Далее либо первый поток проснётся раньше второго, либо второй проснётся раньше первого, либо они проснуться одновременно. Это не будет иметь никакого значения. Их судьба уже решена. Их ожидает взаимная блокировка.
- Просыпается первый поток, и в ходе выполнения 18 строки, передаёт управление методу
last()объектаb. Этот метод пытается автоматически захватить монитор объектаb, но терпит неудачу, поскольку монитор объектаbвсё ещё захвачен вторым потоком, который выполняет методbar()объектаb. Так как необходимый монитор захватить не удалось, первый поток переводится в спящее состояние, ожидая освобождения монитора объектаb. - Просыпается второй поток, и в ходе выполнения 38 строки, передаёт управление методу
last()объектаa. Этот метод пытается автоматически захватить монитор объектаa, но терпит неудачу, поскольку монитор объектаaвсё ещё захвачен первым потоком, который выполняет методfoo()объектаa. Так как необходимый монитор захватить не удалось, второй поток переводится в спящее состояние, ожидая освобождения монитора объектаa. - В данной ситуации совершенно неважно, какой из потоков проснётся раньше, а какой позже, или они вообще проснуться одновременно. Результат всё равно один.
- Каждый поток заснул, ожидая освобождения так необходимого монитора, другим потоком. Но первый поток держит захваченным монитор объекта
a, ожидая освобождения монитора объектаb, а второй поток, держит в захвате монитор объектаb, ожидая освобождения монитора объектаa. - Гонка закончилась. Оба потока не могут продолжать выполнение. Взаимная блокировка. DeadLock.
- Либо первый поток просыпается, и в ходе выполнения метода
last(), объектаb, успевает захватить монитор этого объекта, прежде чем монитор упомянутого объекта будет захвачен вторым потоком во время исполнения методаbar(). - Либо наоборот - второй поток просыпается, и в ходе выполнения метода
last(), объектаa, успевает захватить монитор этого объекта, прежде чем монитор упомянутого объекта будет захвачен первым потоком во время исполнения методаfoo().
last(), после чего другой поток так же, без проблем, продолжит своё исполнение. Но, как я упомянул ранее, это маловероятно, так как потоки, в начале, засыпают на целую секунду (вечность, с точки зрения процессора), а JVM запускает выполнение потоков очень быстро.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
synchronized-методы объекта, автоматически захватывают монитор объекта. Фактически код:Это более удобная форма записи следующего кода:static synchoronizedметоды захватывают монитор самого класса, в котором они объявлены. Т.е. код:будет эквивалентен коду: