Статьи

Параллелизм Java: тупики скрытых потоков

Большинство Java-программистов знакомы с концепцией взаимоблокировки потоков Java . По сути, он включает 2 потока, ожидающих друг друга вечно. Это условие часто является результатом проблем с упорядочиванием блокировок (синхронизированных) или ReentrantLock (чтение или запись).

1
2
3
4
5
6
7
8
Found one Java-level deadlock:
=============================
"pool-1-thread-2":
  waiting to lock monitor 0x0237ada4 (object 0x272200e8, a java.lang.Object),
  which is held by "pool-1-thread-1"
"pool-1-thread-1":
  waiting to lock monitor 0x0237aa64 (object 0x272200f0, a java.lang.Object),
  which is held by "pool-1-thread-2"

Хорошей новостью является то, что HotSpot JVM всегда может обнаружить это условие для вас … или это так? Недавняя проблема взаимоблокировки потоков, затрагивающая производственную среду Oracle Service Bus, вынудила нас вернуться к этой классической проблеме и выявить существование «скрытых» ситуаций тупиковых ситуаций. В этой статье будет продемонстрировано и воспроизведено с помощью простой Java-программы очень специальное условие блокировки блокировки, которое не обнаружено в последней версии HotSpot JVM 1.7. В конце статьи вы также найдете видео, объясняющее пример программы Java и используемый метод устранения неполадок.

Место преступления

Я обычно хотел бы сравнить основные проблемы параллелизма Java с местом преступления, где вы играете главную роль следователя. В этом контексте «преступление» — это фактический сбой в работе ИТ-среды вашего клиента. Ваша работа заключается в:

  • Соберите все доказательства, подсказки и факты (дамп потока, журналы, влияние на бизнес, показатели нагрузки …)
  • Опрос свидетелей и экспертов в области (группа поддержки, служба доставки, поставщик, клиент …)

Следующим этапом вашего расследования является анализ собранной информации и составление потенциального списка одного или нескольких «подозреваемых» вместе с четкими доказательствами. В конце концов, вы хотите сузить его до основной подозреваемой или первопричины. Очевидно, что закон «невиновен до тех пор, пока его вина не будет доказана» здесь не применяется, как раз наоборот. Отсутствие доказательств может помешать вам достичь вышеуказанной цели. Далее вы увидите, что отсутствие обнаружения тупиковых ситуаций JVM Hotspot не обязательно доказывает, что вы не имеете дело с этой проблемой.

Подозреваемый

В этом контексте устранения неполадок «подозреваемый» определяется как код приложения или промежуточного программного обеспечения со следующим проблемным шаблоном выполнения.

  • Использование блокировки FLAT с последующим использованием блокировки ReentrantLock WRITE (путь выполнения # 1)
  • Использование блокировки READRANTLOCK READ с последующим использованием блокировки FLAT (путь выполнения № 2)
  • Параллельное выполнение выполняется двумя потоками Java, но в обратном порядке выполнения

Приведенные выше критерии блокировки блокировки порядка могут быть визуализированы в соответствии с ниже:

Теперь давайте повторим эту проблему с помощью нашего примера Java-программы и посмотрим на вывод дампа потока JVM.

Пример Java-программы

Указанные выше условия взаимоблокировки были впервые обнаружены в нашем проблемном случае Oracle OSB. Затем мы воссоздали его с помощью простой Java-программы. Вы можете скачать весь исходный код нашей программы здесь . Программа просто создает и запускает 2 рабочих потока. Каждый из них выполняет свой путь выполнения и пытается получить блокировки на общих объектах, но в разных порядках. Мы также создали поток детектора взаимоблокировки для мониторинга и регистрации. А пока найдите ниже класс Java, реализующий 2 разных пути выполнения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package org.ph.javaee.training8;
 
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
/**
 * A simple thread task representation
 * @author Pierre-Hugues Charbonneau
 *
 */
public class Task {
       
       // Object used for FLAT lock
       private final Object sharedObject = new Object();
       // ReentrantReadWriteLock used for WRITE & READ locks
       private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
       
       /**
        *  Execution pattern #1
        */
       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 (Throwable any) {}
             
             try {             
                    // 2. Attempt to acquire a Flat lock...
                    synchronized (sharedObject) {}
             }
             // Remove the READ lock
             finally {
                    lock.readLock().unlock();
             }          
             
             System.out.println("executeTask1() :: Work Done!");
       }
       
       /**
        *  Execution pattern #2
        */
       public void executeTask2() {
             
             // 1. Attempt to acquire a Flat lock
             synchronized (sharedObject) {                
                    
                    // Wait 2 seconds to simulate some work...
                    try { Thread.sleep(2000);}catch (Throwable any) {}
                    
                    // 2. Attempt to acquire a WRITE lock                  
                    lock.writeLock().lock();
                    
                    try {
                           // Do nothing
                    }
                    
                    // Remove the WRITE lock
                    finally {
                           lock.writeLock().unlock();
                    }
             }
             
             System.out.println("executeTask2() :: Work Done!");
       }
       
       public ReentrantReadWriteLock getReentrantReadWriteLock() {
             return lock;
       }
}

Как только была запущена ситуация взаимоблокировки, с помощью JVisualVM был создан дамп потока JVM.

Как вы можете видеть из примера дампа потока Java. JVM не обнаружила это состояние взаимоблокировки (например, отсутствие взаимоблокировки на уровне Java Found), но ясно, что эти 2 потока находятся в состоянии взаимоблокировки.

Основная причина: поведение блокировки ReetrantLock READ

Основное объяснение, которое мы нашли в этой точке, связано с использованием блокировки ReadrantLock READ. Блокировки чтения обычно не предназначены для владения. Поскольку нет записи о том, какой поток удерживает блокировку чтения, это, по-видимому, не позволяет логике детектора взаимоблокировки HotSpot JVM обнаруживать взаимоблокировку, включающую блокировки чтения. С тех пор были реализованы некоторые улучшения, но мы видим, что JVM все еще не может обнаружить этот специальный сценарий тупика. Теперь, если мы заменим блокировку чтения (шаблон выполнения № 2) в нашей программе на блокировку записи, JVM наконец обнаружит состояние взаимоблокировки, но почему?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Found one Java-level deadlock:
=============================
"pool-1-thread-2":
  waiting for ownable synchronizer 0x272239c0, (a java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync),
  which is held by "pool-1-thread-1"
"pool-1-thread-1":
  waiting to lock monitor 0x025cad3c (object 0x272236d0, a java.lang.Object),
  which is held by "pool-1-thread-2"
 
Java stack information for the threads listed above:
===================================================
"pool-1-thread-2":
       at sun.misc.Unsafe.park(Native Method)
       - parking to wait for  <0x272239c0> (a java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync)
       at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
       at java.util.concurrent.locks.AbstractQueuedSynchronizer.
parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)
       at java.util.concurrent.locks.AbstractQueuedSynchronizer.
acquireQueued(AbstractQueuedSynchronizer.java:867)
       at java.util.concurrent.locks.AbstractQueuedSynchronizer.
acquire(AbstractQueuedSynchronizer.java:1197)
       at java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock.lock(ReentrantReadWriteLock.java:945)
       at org.ph.javaee.training8.Task.executeTask2(Task.java:54)
       - locked <0x272236d0> (a java.lang.Object)
       at org.ph.javaee.training8.WorkerThread2.run(WorkerThread2.java:29)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
       at java.lang.Thread.run(Thread.java:722)
"pool-1-thread-1":
       at org.ph.javaee.training8.Task.executeTask1(Task.java:31)
       - waiting to lock <0x272236d0> (a java.lang.Object)
       at org.ph.javaee.training8.WorkerThread1.run(WorkerThread1.java:29)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
       at java.lang.Thread.run(Thread.java:722)

Это связано с тем, что блокировки записи отслеживаются JVM, как плоские блокировки. Это означает, что детектор взаимоблокировки HotSpot JVM в настоящее время предназначен для обнаружения:

  • Тупик на объектных мониторах с блокировками FLAT
  • Дедлок, связанный с заблокированными собственными синхронизаторами, связанными с блокировками WRITE

Отсутствие отслеживания блокировки чтения для каждого потока, по-видимому, предотвращает обнаружение взаимоблокировки для этого сценария и значительно увеличивает сложность устранения неполадок. Я предлагаю вам прочитать комментарии Дуга Ли по всей этой проблеме, поскольку в 2005 году были высказаны опасения относительно возможности добавления отслеживания удержания чтения для каждого потока из-за некоторой потенциальной накладной блокировки. Ниже приведены мои рекомендации по устранению неполадок, если вы подозреваете наличие скрытой тупиковой ситуации, связанной с блокировками чтения:

  • Тщательно проанализируйте трассировку стека вызовов потока, это может выявить некоторый код, который может получить блокировки чтения и помешать другим потокам получить блокировки записи.
  • Если вы являетесь владельцем кода, следите за количеством блокировок чтения с помощью метода lock.getReadLockCount ()

Я с нетерпением жду ваших отзывов, особенно от людей, имеющих опыт работы с этим типом тупиковых ситуаций, связанных с блокировками чтения. Наконец, найдите ниже видео, объясняющее такие результаты посредством выполнения и мониторинга нашей программы Java.

Ссылка: параллелизм Java: скрытые потоки блокируются от нашего партнера по JCG Пьера-Хьюга Шарбонно в блоге, посвященном шаблонам поддержки Java EE и учебному пособию по Java .