Синхронизированный блок предотвращает одновременное выполнение кода на одном и том же объекте монитора. В этом случае doS1()
объект монитора равен resource1
, а в doS2 () объект монитора равен resource2
. Когда поток входит в синхронизированный блок, он пытается получить блокировку объекта монитора. Если он получит блокировку, он продолжит работу и снимет блокировку только тогда, когда выйдет из блока (или снимет блокировку) Если он не может получить блокировку (поскольку другой поток уже имеет блокировку, то поток будет блокироваться до тех пор, пока блокировка не будет снята и он не сможет ее получить).
Два приведенных выше примера, Deadlock1
и Deadlock2
, не выполняют эквивалентный код. В Deadllock1
оба объекта монитора не могут быть получены одним и тем же потоком в одно и то же время.
В Deadlock2
каждый поток пытается одновременно заблокировать оба объекта монитора.
- Поток 1 получает блокировку для ресурса1 и спит в течение 50 мс
- В то же время Поток 2 получает блокировку для ресурса2 и спит в течение 50 мс
- Когда поток 1 продолжается, он все еще имеет блокировку для resource1 и пытается получить блокировку для resource2. Поток resource2 по-прежнему удерживается потоком 2, поэтому он блокирует ожидание освобождения resource2.
- Тем временем поток 2 пытается получить блокировку для ресурса1, но он все еще удерживается потоком 1, поэтому он блокирует ожидание, пока поток 1 освободит монитор для ресурса1.
- Теперь оба потока блокируются в ожидании освобождения объекта монитора, и, поскольку они оба заблокированы, они не могут освободить заблокированный объект монитора ... отсюда и тупик.
Если мы переписываем Deadlock1
, чтобы имитировать функциональность Deadlock2
, так что это приводит к тупику, это будет выглядеть так:
public class Deadlock1 {
static class Client {
final Object resource1 = "resource1";
final Object resource2 = "resource2";
void doS1() {
synchronized (resource1) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
doS2();
}
}
void doS2() {
synchronized (resource2) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
doS1();
}
}
}
public static void main(String[] args) {
Client client = new Client();
new Thread(client::doS1).start();
new Thread(client::doS2).start();
}
}
В качестве альтернативы, если мы переписываем Deadlock2
, чтобы имитировать функциональность Deadlock1
, чтобы он не приводил к тупику, это выглядело бы так:
public class Deadlock2 {
static class Client {
final Object resource1 = "resource1";
final Object resource2 = "resource2";
}
public static void main(String[] args) {
Client client = new Client();
new Thread(
() ->
{
synchronized (client.resource1) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
}
synchronized (client.resource2) {}
}).start();
new Thread(
() ->
{
synchronized (client.resource2) {
try {
System.out.println("3");
Thread.sleep(50);
} catch (InterruptedException e) {
}
}
synchronized (client.resource1) {}
}).start();
}
}