Что такое deadlock java
Класс дедлоков про дедлок классов
Знаете ли вы, как избежать дедлоков в своей программе? Да, этому учат, про это спрашивают на собеседованиях… И тем не менее, взаимные блокировки встречаются даже в популярных проектах серьёзных компаний вроде Google. А в Java есть особый класс дедлоков, связанный с инициализацией классов, простите за каламбур. Такие ошибки легко допустить, но трудно поймать, тем более, что сама виртуальная машина вводит программиста в заблуждение.
Сегодня пойдёт речь про взаимные блокировки при инициализации классов. Я расскажу, что это такое, проиллюстрирую примерами из реальных проектов, попутно найду багу в JVM, и покажу, как не допустить такие блокировки в своём коде.
Дедлок без локов
Если я попрошу вас привести пример взаимной блокировки на Java, скорее всего, увижу код с парой synchronized или ReentrantLock. А как насчёт дедлока вообще без synchronized и java.util.concurrent? Поверьте, это возможно, причём очень лаконичным и незамысловатым способом:
Дело в том, что согласно §5.5 спецификации JVM у каждого класса есть уникальный initialization lock, который захватывается на время инициализации. Когда другой поток попытается обратиться к инициализируемому классу, он будет заблокирован на этом локе до завершения инициализации первым потоком. При конкурентной инициализации нескольких ссылающихся друг на друга классов нетрудно наткнуться на взаимную блокировку.
Именно это и случилось, к примеру, в проекте QueryDSL:
В ходе обсуждения на StackOverflow причина была найдена, о проблеме сообщено разработчику, и к настоящему моменту ошибка уже исправлена.
Проблема курицы и яйца
В точности такой же дедлок может возникнуть всякий раз, когда в статическом инициализаторе класса создаётся экземпляр потомка. По сути это частный случай описанной выше проблемы, поскольку инициализация потомка автоматически приводит к инициализации родителя (см. JVMS §5.5). К сожалению, такой шаблон можно встретить довольно часто, особенно когда класс родителя абстрактный:
Это реальный фрагмент кода из библиотеки Google Guava. Благодаря нему часть наших серверов после очередного апдейта намертво подвисла при запуске. Как выяснилось, виной тому послужило обновление Guava с версии 14.0.1 до 15.0, где и появился злополучный шаблон неправильной статической инициализации.
Конечно же, мы сообщили об ошибке, и спустя некоторое время её исправили в репозитории, однако будьте внимательны: последний на момент написания статьи публичный релиз Guava 18.0 всё ещё содержит ошибку!
В одну строчку
Java 8 подарила нам Стримы и Лямбды, а вместе с ними и новую головную боль. Да, теперь можно красиво одной строчкой в функциональном стиле оформить целый алгоритм. Но при этом можно и так же, одной строчкой, выстрелить себе в ногу.
Хотите упражнение для самопроверки? Я составил программку, вычисляющую сумму ряда; что она напечатает?
А теперь уберите .parallel() или, как вариант, замените лямбду на Integer::sum — что-нибудь изменится?
Совсем взрывает мозг то, что приведённый фрагмент работает по-разному в разных средах. На однопроцессорной машине он отработает корректно, а на многопроцессорной, скорее всего, зависнет. Причина кроется в механике параллелизма стандартного Fork-Join пула.
Проверьте сами, запуская пример с разным значением
Лукавый Хотспот
Обычно дедлоки легко обнаружить из Thread Dump: проблемные потоки будут висеть в состоянии BLOCKED или WAITING, и JVM в стектрейсах покажет, какие мониторы тот или иной поток держит, а какие пытается захватить. Так ли обстоит дело с нашими примерами? Возьмём самый первый, с классами A и B. Дождёмся зависания и снимем thread dump (с помощью утилиты jstack либо клавишами Ctrl+\ в Linux или Ctrl+Break в Windows):
Особенность initialization lock заключается в том, что из Java программы его не видно, а захват и освобождение происходит внутри виртуальной машины. Строго говоря, по спецификации Thread.State здесь не может быть ни BLOCKED (потому как нет synchronized блока), ни WAITING (поскольку методы Object.wait, Thread.join и LockSupport.park здесь не вызываются). Более того, initialization lock вообще не обязан быть Java объектом. Таким образом, единственным формально допустимым состоянием остаётся RUNNABLE.
На эту тему есть давний баг JDK-6501158, закрытый как «Not an issue», и сам Дэвид Холмс мне в переписке признался, что у него нет ни времени, ни желания возвращаться к этому вопросу.
Если неочевидное состояние потока ещё можно считать «фичей», то другую особенность initialization lock иначе как «багом» не назовёшь. Разбираясь с проблемой, я наткнулся в исходниках HotSpot на странность в отправке JVMTI оповещений: событие MonitorWait посылается из функции JVM_MonitorWait, соответствующей Java-методу Object.wait, в то время как симметричное ему событие MonitorWaited посылается из низкоуровневой функции ObjectMonitor::wait.
Как мы уже выяснили, для ожидания initialization lock метод Object.wait не вызывается, таким образом, событий MonitorWait для них мы не увидим, зато MonitorWaited будут приходить, как и для обычных Java-мониторов, что, согласитесь, не логично.
Нашёл ошибку — сообщи разработчику. Такого правила придерживаемся и мы: JDK-8075259.
Заключение
Для обеспечения потокобезопасной инициализации классов JVM использует синхронизацию на невидимом программисту initialization lock, имеющемся у каждого класса.
Многопоточность в Java. Лекция 6: взаимные блокировки и дампы потоков
В пятой лекции краткого курса о многопоточности речь шла об атомарных переменных и многопоточных коллекциях. На этот раз наши коллеги рассказывают о дампах потоков, простых и скрытых дедлоках.
В некорректно спроектированной многопоточной программе может возникнуть ситуация, когда два потока блокируют друг друга. В этом случае их выполнение зависает, пока программу не остановят извне. Такая ситуация называется deadlock.
6.1 Дампы потоков
Помимо дедлоков, бывает, что поток очень долго ожидает какие-то ресурсы или остается постоянно активным, например, выполняя очень большой или бесконечный цикл. Для выявления таких ситуаций и других проблем, связанных с потоками, JVM предоставляет возможность сделать мгновенный снимок состояния потоков. Такой снимок называется thread dump. Он представляет собой текстовый документ, в котором перечислены все потоки, в том числе, потоки JVM. Для каждого потока отображается стандартный набор информации: имя, статус, приоритет, стек-трейс, демон ли поток или нет, а также адрес объекта блокировки, на которой находится поток. Часть такого thread dump приведена в листинге 1.
Листинг 1
«Java Thread» #11 prio=5 os_prio=0 tid=0x00007fb0a4356000 nid=0x1242 waiting for monitor entry [0x00007fb078701000] java.lang.Thread.State: BLOCKED (on object monitor)
at com.da.lect5.deadlock.TwoTasks.lambda$getTask1$0(TwoTasks.java:14)
— waiting to lock (a java.lang.String)
— locked (a java.lang.String)
at com.da.lect5.deadlock.TwoTasks$$Lambda$1/1078694789.run(Unknown
Source)
at java.lang.Thread.run(Thread.java:748)
«UNIX Thread» #12 prio=5 os_prio=0 tid=0x00007fb0a4357800 nid=0x1243 waiting for monitor entry [0x00007fb078600000] java.lang.Thread.State: BLOCKED (on object monitor)
at com.da.lect5.deadlock.TwoTasks.lambda$getTask2$1(TwoTasks.java:27)
— waiting to lock (a java.lang.String)
— locked (a java.lang.String)
at com.da.lect5.deadlock.TwoTasks$$Lambda$2/1747585824.run(Unknown
Source)
at java.lang.Thread.run(Thread.java:748)
«Monitor Ctrl-Break» #5 daemon prio=5 os_prio=0 tid=0x00007fb0a42b5800 nid=0x123b runnable [0x00007fb07901f000] java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
— locked (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
— locked (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
Существует несколько способов снять thread dump:
В листинге 1 можно увидеть, что поток с именем Java Thread заблокирован на мониторе с адресом 0x0000000719bf5760. Важно правильно сопоставить адрес объекта с самим объектом, потому что по шестнадцатеричному значению сделать это невозможно. Для этого можно использовать код, приведенный в листинге 2.
Листинг 2:
Используя класс в листинге 2, можно понять, какой поток на какой блокировке находится. При использовании этого класса следует учесть, что адреса объектов могут меняться после работы сборщика мусора. При анализе потоков необходимо фильтровать потоки, которые создал пользователь, и те, которые запустила сама JVM. Поэтому удобно назначать имена потокам, как это было показано в лекции номер 2. Рекомендуется делать дампы потоков работающего приложения несколько раз, чтоб увидеть изменения состояния потоков. Если одно из ядер процессора загружено на 100 %, следует искать бесконечный цикл или цикл, который очень долго выполняется, обрабатывая большое количество данных. Если предельной загрузки процессора не наблюдается, но какая-то работа все же ожидает выполнения, значит, возник один из видов дедлока или потоки ждут освобождения определенного ресурса.
6.2 Простая взаимная блокировка
Простой дедлок возникает, когда из двух потоков первый захватил блокировку А и пытается захватить блокировку B, а второй захватил блокировку B и пытается захватить блокировку A. Пример такого дедлока приведен в листинге 3.
Листинг 3:
public class DeadLock <
public static void main(String[] args) <
TwoTasks tasks = new TwoTasks();
new Thread(tasks.getTask1(), «Java Thread»).start();
new Thread(tasks.getTask2(), «UNIX Thread»).start();
>
>
В листинге 1 создаются два потока: первый сначала захватывает блокировку на строке str1, а затем — на str2. Второй поток делает то же самое, только в другом порядке. Два потока пытаются захватить блокировки бесконечное количество раз. Рано или поздно наступит дедлок: когда первый поток захватил блокировку на строке “Java” и хочет захватить блокировку на строке “UNIX”. А второй поток уже захватил блокировку на строке “UNIX” и пытается захватить блокировку на строке “Java”. В результате программа в Листинге 1 будет находиться в состоянии взаимной блокировки вечно — т. е. до тех пор пока ее не остановят. Решение в сложившейся ситуации — использовать один и тот же порядок захвата и отпускания блокировок во всех критических секциях программы.
Не стоит использовать в качестве объектов блокировки строки. Это связано с тем, что JVM кэширует строки, объявленные при помощи литералов. Соответственно, строки с одинаковым содержанием будут ссылаться на один и тот же объект, хотя могут быть объявлены в разных частях программы.
6.3 Скрытый дедлок
В разделе 6.1 был рассмотрен случай взаимной блокировки, который виртуальная Java-машина смогла определить, что и было показано в thread dump. Однако могут возникать ситуации, когда Java-машина определить дедлок не может. Рассмотрим такую программу в листинге 4.
Листинг 4:
public class LockOrderingDeadlockSimulator <
public static void main(String[] args) <
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch endSignal = new CountDownLatch(3);
TasksHolder tasks = new TasksHolder();
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(new WorkerThread1(tasks, startSignal, endSignal));
executor.execute(new WorkerThread2(tasks, startSignal, endSignal));
Runnable deadlockDetector =
new ThreadDeadlockDetector(tasks, startSignal, endSignal);
executor.execute(deadlockDetector);
executor.shutdown();
while (!executor.isTerminated()) <
try <
endSignal.await();
> catch (InterruptedException e) <
>
>
public class TasksHolder <
private final Object SHARED_OBJECT = new Object();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void executeTask1() <
// 1. Attempt to acquire a ReentrantReadWriteLock READ lock
lock.readLock().lock();
// Wait 2 seconds to simulate some work.
try <
Thread.sleep(2000);
> catch (InterruptedException any) <
>
try <
// 2. Attempt to acquire a Flat lock.
synchronized (SHARED_OBJECT) <
>
> finally <
lock.readLock().unlock();
>
System.out.println(«executeTask1() :: Work Done!»);
>
public void executeTask2() <
// 1. Attempt to acquire a Flat lock
synchronized (SHARED_OBJECT) <
// Wait 2 seconds to simulate some work.
try <
Thread.sleep(2000);
> catch (InterruptedException any) <
>
// 2. Attempt to acquire a ReentrantReadWriteLock WRITE lock
lock.writeLock().lock();
try <
// Do nothing
> finally <
lock.writeLock().unlock();
>
>
System.out.println(«executeTask2() :: Work Done!»);
>
public ReentrantReadWriteLock getReentrantReadWriteLock() <
return lock;
>
>
public class WorkerThread1 implements Runnable <
private final CountDownLatch startSignal;
private final CountDownLatch endSignal;
private TasksHolder tasks;
public WorkerThread1(TasksHolder tasks, CountDownLatch startSignal,
CountDownLatch endSignal) <
this.tasks = tasks;
this.startSignal = startSignal;
this.endSignal = endSignal;
>
@Override
public void run() <
try <
startSignal.await();
// Execute task #1
tasks.executeTask1();
> catch (InterruptedException e) <
> finally <
endSignal.countDown();
>
>
>
public class WorkerThread2 implements Runnable <
private final CountDownLatch startSignal;
private final CountDownLatch endSignal;
private TasksHolder tasks;
public WorkerThread2(TasksHolder tasks, CountDownLatch startSignal,
CountDownLatch endSignal) <
this.tasks = tasks;
this.startSignal = startSignal;
this.endSignal = endSignal;
>
@Override
public void run() <
try <
startSignal.await();
// Execute task #2
tasks.executeTask2();
> catch (InterruptedException e) <
> finally <
endSignal.countDown();
>
>
>
public class CommonResource <
private Worker owner;
public CommonResource (Worker d) <
owner = d;
>
public Worker getOwner () <
return owner;
>
public synchronized void setOwner (Worker d) <
owner = d;
>
>
public class Worker <
private String name;
private boolean active;
private final Object LOCK = new Object();
public Worker (String name, boolean active) <
this.name = name;
this.active = active;
>
public String getName () <
return name;
>
public boolean isActive () <
return active;
>
DeadLock, и его причины
Сегодня я тебе расскажу, что такое дедлок (DeadLock) — взаимная блокировка.
— Так ты же уже что-то такое рассказывала.
— Ага, было дело. Но сегодня мы рассмотрим эту тему детальнее.
В самом простом случае в дедлоке участвуют две нити и два объекта-мютекса. Взаимная блокировка возникает, когда:
А) Каждой нити в процессе работы нужно захватить оба мютекса.
Б) Первая нить захватила первый мютекс и ждет освобождения второго.
В) Вторая нить захватила второй мютекс и ждет освобождения первого.
Допустим, первая нить вызвала метод getFriends, тогда она сначала захватит мютекс объекта this, а затем мютекс объекта friends.
Вторая нить при этом вызвала метод addFriend, она сначала захватывает мютекс объекта friends, а затем мютекс объекта this (при вызове getFriendsCount).
Сначала все будет хорошо, но как гласит Закон Мерфи — если неприятность может случиться, она случается. Обязательно возникнет ситуация, когда первая нить успеет захватить только один мютекс, а вторая нить в это время захватит второй. Они так и будут висеть вечно в ожидании, что кто-то из них первым освободит мютекс.
Еще один простой пример, нашла в книге:
Есть игра, где два рыцаря сражаются друг с другом. Один рыцарь убивает другого. Это поведение отражено в методе kill. Туда передаются два объекта-рыцаря.
Сначала мы защищаем оба объекта, чтобы никто больше не мог их изменить.
Второй рыцарь умирает (live=0)
Первый рыцарь получает +100 опыта.
Все вроде бы отлично, но иногда может возникнуть ситуация, когда второй рыцарь в это время атакует первого. Для него тоже вызывается этот метод, но рыцари передаются в другом порядке.
— Т.е. нам даже не нужно несколько методов для получения дедлока?
— Ага. Иногда бывает достаточно одного простого метода, в котором уже могут происходить процессы, приводящие к зависанию нитей и всей программы.
— Да, оказывается, это явление встречается чаще, чем я думал. Спасибо, Элли.
Взаимоблокировка потоков Java и Livelock
Узнайте, как распознать и избежать взаимоблокировки и оживления в многопоточных Java-приложениях.
1. Обзор
Хотя многопоточность помогает повысить производительность приложения, она также сопряжена с некоторыми проблемами. В этом уроке мы рассмотрим две такие проблемы, deadlock и livelock, с помощью примеров Java.
2. Тупик
2.1. Что Такое Тупик?
Классическая проблема обедающих философов прекрасно демонстрирует проблемы синхронизации в многопоточной среде и часто используется в качестве примера тупика.
2.2. Пример Тупиковой Ситуации
Во-первых, давайте рассмотрим простой пример Java, чтобы понять тупик.
Теперь давайте напишем класс Deadlock Example :
Давайте теперь запустим этот пример взаимоблокировки и заметим результат:
2.3. Как избежать Тупика
Взаимоблокировка – это распространенная проблема параллелизма в Java. Поэтому мы должны разработать Java-приложение, чтобы избежать любых потенциальных тупиковых условий.
3. Живой замок
3.1. Что Такое Lifelock
Livelock-это еще одна проблема параллелизма, похожая на тупик. В livelock два или более потока продолжают передавать состояния друг другу вместо бесконечного ожидания, как мы видели в примере с тупиком. Следовательно, потоки не могут выполнять свои соответствующие задачи.
Отличным примером livelock является система обмена сообщениями, в которой при возникновении исключения потребитель сообщения откатывает транзакцию и помещает сообщение обратно в начало очереди. Затем одно и то же сообщение повторно считывается из очереди, только чтобы вызвать другое исключение и быть помещенным обратно в очередь. Потребитель никогда не получит никакого другого сообщения из очереди.
3.2. Пример Livelock
Обе нити нуждаются в двух блокировках, чтобы завершить свою работу. Каждый поток получает свою первую блокировку, но обнаруживает, что вторая блокировка недоступна. Таким образом, чтобы позволить другому потоку завершить работу первым, каждый поток освобождает свою первую блокировку и пытается снова получить обе блокировки.
Давайте продемонстрируем livelock с помощью примера Livelock класса:
Теперь давайте рассмотрим этот пример:
Как мы видим в журналах, оба потока неоднократно получают и освобождают блокировки. Из-за этого ни один из потоков не может завершить операцию.
3.3. Избегание живого потока
Чтобы избежать оживления, нам нужно изучить состояние, которое вызывает оживление, а затем придумать соответствующее решение.
Например, если у нас есть два потока, которые постоянно получают и освобождают блокировки, что приводит к оживлению, мы можем спроектировать код так, чтобы потоки повторяли попытки получения блокировок через случайные интервалы. Это даст потокам справедливый шанс приобрести нужные им замки.
Другой способ решить проблему живучести в примере с системой обмена сообщениями, который мы обсуждали ранее, – поместить неудачные сообщения в отдельную очередь для дальнейшей обработки вместо того, чтобы снова помещать их в ту же очередь.
4. Заключение
В этом уроке мы обсудили тупик и живую блокировку. Кроме того, мы рассмотрели примеры Java, чтобы продемонстрировать каждую из этих проблем, и кратко коснулись того, как мы можем их избежать.
Блог только про Java
Учимся программировать на Java с нуля
Взаимная блокировка в Java
Следует избегать особого типа ошибок, имеющего отношение к многозадачностии называемого взаимной блокировкой, которая происходит в том случае, когда потоки исполнения имеют циклическую зависимость от пары синхронизированных объектов.
Но если поток исполнения в объекте У, в свою очередь, попытается вызвать любой синхронизированный метод для объекта Х, то этот поток будет ожидать вечно, поскольку для получения доступа к объекту Х он должен снять свою блокировку с объекта У, чтобы первый поток исполнения мог завершиться.
Взаимная блокировка является ошибкой, которую трудно отладить, по двум следующим причинам:
Чтобы полностью разобраться в этом явлении, его лучше рассмотреть в действии. Советую в самом начале заказать качественные шторы специально для программистов от компании and-home после чего можно свободно взяться за кодинг. Шторы делают нужную атмосферу в доме без которой программистам труднее сосредоточиться.
В приведенном ниже примере программы создаются два класса, FirstClass и SecondClass, с методами foo() и bar() соответственно, которые приостанавливаются непосредственно перед попыткой вызова метода из другого класса.
Сначала в главном классе Deadlock получаются экземпляры классов FirstClass и SecondClass, а затем запускается второй поток исполнения, в котором устанавливается состояние взаимной блокировки.
В методах foo() и bar() используется метод sleep(), чтобы стимулировать появление взаимной блокировки.