Java-разработчик Владимир Фролов и Android-разработчик Никита Сизинцев из DataArt, составили краткий курс по многопоточности в Java из шести лекций
В некорректно спроектированной многопоточной программе может возникнуть ситуация, когда два потока блокируют друг друга. В этом случае их выполнение зависает, пока программу не остановят извне. Такая ситуация называется 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 <0x0000000719bf5760> (a java.lang.String)
- locked <0x0000000719bf5730> (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 <0x0000000719bf5730> (a java.lang.String)
- locked <0x0000000719bf5760> (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 <0x0000000719db96b8> (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 <0x0000000719db96b8> (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:
- Сначала узнать PID процесса Java-программы. Это можно сделать, вызвав утилиту jps из папки bin, где установлена JDK. После этого выполнить команду jstack -F <PID>.
- Использовать JVisiualVM.
- Использовать программу Java Mission Control.
- В IntelliJ Idea обратиться к окну Run при запущенной программе.
В листинге 1 можно увидеть, что поток с именем Java Thread заблокирован на мониторе с адресом 0x0000000719bf5760. Важно правильно сопоставить адрес объекта с самим объектом, потому что по шестнадцатеричному значению сделать это невозможно. Для этого можно использовать код, приведенный в листинге 2.
Листинг 2:
public class AddressUtil {
private static Unsafe getUnsafeObj() {
Unsafe unsafe = null;
> try {
Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception exception) {
exception.printStackTrace();
}
return unsafe;
}
private static void printAddresses(String label, Object... objects) {
Unsafe unsafe = getUnsafeObj();
if (Objects.nonNull(unsafe)) {
final boolean is64bit = unsafe.addressSize() == 8;
System.out.print(label + ": 0x");
long last = 0;
int offset = unsafe.arrayBaseOffset(objects.getClass());
int scale = unsafe.arrayIndexScale(objects.getClass());
switch (scale) {
case 4:
long factor = is64bit ? 8 : 1;
final long i1 = (unsafe.getInt(objects, offset) & 0xFFFFFFFFL) * factor;
System.out.print(Long.toHexString(i1));
last = i1;
for (int i = 1; i < objects.length; i++) {
final long i2 =
(unsafe.getInt(objects, offset + i * 4) & 0xFFFFFFFFL) * factor;
if (i2 > last) {
System.out.print(", +" + Long.toHexString(i2 - last));
} else {
System.out.print(", -" + Long.toHexString( last - i2));
}
last = i2;
}
break;
case 8:
throw new AssertionError("Not supported");
}
}
}
}
Используя класс в листинге 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();
}
}
public class TwoTasks {
private String str1 = "Java";
private String str2 = "UNIX";
@SuppressWarnings("Duplicates")
public Runnable getTask1() {
return () -> {
while (true) {
synchronized (str1) {
synchronized (str2) {
System.out.println(str1 + str2);
}
}
}
};
}
@SuppressWarnings("Duplicates")
public Runnable getTask2() {
return () -> {
while (true) {
synchronized (str2) {
synchronized (str1) {
System.out.println(str2 + str1);
}
}
}
};
}
}
В листинге 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();
startSignal.countDown();
while (!executor.isTerminated()) {
try {
endSignal.await();
} catch (InterruptedException e) {
}
}
System.out.println("LockOrderingDeadlockSimulator done!");
}
}
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 ThreadDeadlockDetector implements Runnable {
В программе, приведенной в листинге 4, создаются три потока:
private TasksHolder tasks;
private final CountDownLatch startSignal;
private final CountDownLatch endSignal;
public ThreadDeadlockDetector(TasksHolder tasks, CountDownLatch startSignal,
CountDownLatch endSignal) {
this.tasks = tasks;
this.startSignal = startSignal;
this.endSignal = endSignal;
}
@Override
public void run() {
try {
startSignal.await();
// Perform 10 iterations with 2 seconds elapsed time
for (int i = 0; i < 10; i++) {
// 1. Flat & Reetrant WRITE lock deadlock detection
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] threadIds = threadBean.findDeadlockedThreads();
int deadlockedThreads = threadIds != null ? threadIds.length : 0;
System.out.println("\n** Deadlock detectin in progress...");
System.out.println("Deadlocked threads found by the HotSpot JVM: " +
deadlockedThreads);
// 2. Reetrant READ lock tracking
System.out.println("READ lock count: " +
tasks.getReentrantReadWriteLock().getReadLockCount());
Thread.sleep(2000);
}
} catch (InterruptedException e) {
} finally {
endSignal.countDown();
}
}
}
- В потоке ThreadDeadlockDetector с помощью MXBean 10 раз с интервалом в две секунды получается информация о потоках, которые находятся в состоянии дедлока, эта информация выводится на консоль.
- В потоке, который выполняет task1, сначала захватывается блокировка на чтение, а затем блокировка с помощью ключевого слова synchronized.
- В потоке, который выполняет task2, сначала захватывается блокировка с использованием ключевого слова synchronized, а после — блокировка на запись.
Возникает дедлок, но поток ThreadDeadlockDetector его не находит. Дело в том, что readLock устроен так, что ассоциации между потоком и используемой им блокировкой readlock не возникает. Когда поток использует writeLock или блокировку на основании ключевого слова synchronized, такая связь есть. Если в листинге 4 заменить readLock на writeLock, поток для мониторинга взаимную блокировку обнаружит.
6.3 LIVELOCK
Livelock — рекурсивная ситуация, когда два или более потоков выполняют один и тот же код, и этот код хочет передать управление другому потоку. Livelock, как и обычный дедлок, остановит выполнение программы. Ключевая разница — при Livelock потоки не блокируются: они продолжают потреблять процессорное время, хотя выполнение программы при этом заблокировано.
В реальной жизни аналогией может служить ситуация, когда один человек хочет выйти, а другой — войти в помещение через одну и ту же дверь. Если оба попытаются уступить друг другу очередь, ни один так и не сможет ни войти, ни выйти. Схематически Livelock показан на рисунке 1:
Пример программы — в листинге 5.
Листинг 5:
public class LiveLock {
public static void main (String[] args) {
final Worker worker1 = new Worker("Worker 1 ", true);
final Worker worker2 = new Worker("Worker 2", true);
final CommonResource res = new CommonResource(worker1);
new Thread(() -> worker1.work(res, worker2)).start();
new Thread(() -> worker2.work(res, worker1)).start();
}
}
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;
}
public void work (CommonResource commonResource, Worker otherWorker) {
synchronized (LOCK) {
while (active) {
// wait for the resource to become available.
if (commonResource.getOwner() != this) {
try {
LOCK.wait(10);
} catch (InterruptedException e) {
}
continue;
}
> // If other worker is also active let it do it's work first
if (otherWorker.isActive()) {
System.out.println(getName() +
" : handover the resource to the worker " + otherWorker.getName());
commonResource.setOwner(otherWorker);
continue;
}
> //now use the commonResource
System.out.println(getName() + ": working on the common resource");
active = false;
commonResource.setOwner(otherWorker);
}
}
}
}
0 комментариев
Добавить комментарий