ReentrantReadWriteLock зависает при использовании в ConcurrentHashMap :: compute () - PullRequest
0 голосов
/ 23 октября 2019

TL; DR - В моем приложении многие потоки захватывают ReentrantReadWriteLock в режиме READ, в то время как они вставляют записи в ConcurrentHashMap через метод compute (), и снимают блокировку READ после завершения lamdba, переданного в compute (). Существует отдельный поток, который захватывает ReentrantReadWriteLock в режиме WRITE и очень (очень) быстро освобождает его. В то время как все это происходит, ConcurrentHashMap изменяет размеры (растет И сокращается). Я сталкиваюсь с зависанием и всегда вижу ConcurrentHashMap :: Transfer (), который вызывается во время изменения размера, в трассировке стека. Все потоки заблокированы, ожидая, чтобы захватить МОЙ ReentrantReadWriteLock. Репродуктор по адресу: https://github.com/rumpelstiltzkin/jdk_locking_bug

Я что-то не так делаю в соответствии с задокументированным поведением, или это ошибка JDK? Обратите внимание, что я НЕ запрашиваю другие способы реализации моего приложения.


Подробности: Вот некоторый контекст, объясняющий, почему мое приложение делает то, что делает. Код воспроизводителя - это урезанная версия для демонстрации проблемы.

Мое приложение имеет сквозной кэш. Записи вставляются в кэш с отметкой времени, когда они вставляются, и отдельный поток очистки выполняет итерации в кэше, чтобы найти записи, которые были созданы после того, как поток очистки потока сохранил записи на диске, т.е. после последнего времени сброса. Кеш - это не что иное, как ConcurrentHashMap.

Теперь возможна гонка, при которой запись создается с меткой времени tX, и пока она вставляется в ConcurrentHashMap, поток-итератор выполняет итерации кеша, а -not найти запись (она все еще вставляется, поэтому еще не видна в Map :: Iterator потока-сбрасывателя), и поэтому она не сохраняется и переводит время последнего сброса в tY, так что tY> tX. В следующий раз, когда поток-очиститель выполняет итерации кеша, он не сочтет запись tX-timestamp нужной для очистки, и мы пропустим ее сохранение. В конце концов, tX будет очень старой отметкой времени, и кеш будет безвозвратно утрачивать это обновление.

Чтобы обойти эту проблему, потоки, которые обновляют кеш новыми записями, захватывают ReentrantReadWriteLock в режиме READ внутри лямбды, которая создаетзапись в кеше внутри метода ConcurrentHashMap :: compute (), и поток-мусорщик захватывает тот же ReentrantReadWriteLock в режиме WRITE при захвате времени последнего сброса. Это гарантирует, что, когда поток-очиститель получает метку времени, все объекты «видны» на Карте и имеют метку времени <= время последнего сброса. </p>


Воспроизведение в моей системе:

$> java -version
openjdk version "1.8.0_191"
OpenJDK Runtime Environment (build 1.8.0_191-b12)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)
$> ./runtest.sh 
seed is 1571855560640
Main spawning 100 readers
Main spawned 100 readers
Main spawning a writer
Main spawned a writer
Main waiting for threads ... <== hung

Все потоки (читатели и писатели) заблокированы в ожидании 0x00000000c6511648

$> ps -ef | grep java | grep -v grep
user   54896  54895  0 18:32 pts/1    00:00:07 java -ea -cp target/*:target/lib/* com.hammerspace.jdk.locking.Main

$> jstack -l 54896 > jstack.1

$> grep -B3 'parking to wait for  <0x00000000c6511648>' jstack.1  | grep tid | head -10
"WRITER" #109 ...
"READER_99" ...
...

'top' показывает, что мой процесс Java спал в течение нескольких минут (он использует чуть-чутьИнкрементное увеличение процессора для возможного переключения контекста, а что нет - см. Справочную страницу сверху для более подробного объяснения, почему это происходит)

$> top -p 54896
   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                               
 54896 user      20   0 4630492 103988  12628 S   0.3  2.7   0:07.37 java -ea -cp target/*:target/lib/* com.hammerspace.jdk.locking.Main

1 Ответ

0 голосов
/ 23 октября 2019

Примечание : Ниже приведен список наблюдений, предлагаемый подход и рекомендации по исправлению ошибки в Oracle. Не решение.

Наблюдения

  1. Параллельные карты имеют встроенный механизм блокировки, нам не нужно приобретать его самим

  2. Атомные * классывозврат в течение «одного» цикла ЦП и, следовательно, нет необходимости в получении блокировки при работе с ними

  3. В Cache.java вы получаете (свой собственный) ReadLock для обновления кэша (Строка 34) и (ваш собственный) WriteLock для чтения с карты (строка 58) и не получения какой-либо блокировки, когда вы фактически удаляете отображение (строка 71).

  4. ОдновременноИтераторы карт слабо согласованы и не будут видеть ваши обновления, даже если вставка завершена. это специально.

  5. Я восстановил AtomicInteger, так как не хотел использовать Holder (из jax-ws) и не смог воспроизвести блокировку вашего потока.

  6. Учитывая, что вы запускаете потоки получения ReadLock перед запуском потоков получения WriteLock. Потоки получения WriteLock никогда не получат шанса на запуск, поскольку уже есть группа потоков, которые уже получили блокировки чтения.

  7. Введение 1-секундного ожидания в методе обновления Cache # после освобожденияReadLock дал возможность запустить потоки получения WriteLock.

  8. Я отменил свои обновления и смог воспроизвести вашу проблему. Но я видел образец.

    a. Использование Holder для lockCount заставило систему сканировать в кратчайшие сроки.

    b. Использование AtomicInteger для lockCount продлило жизнь еще на несколько секунд

    c. Введение консольного заявления о получении и освобождении замков с идентификатором работоспособности продлило жизнь на одну или две минуты.

    d. Замена идентификатора на имя текущего потока в выводе консоли полностью устранила проблему.

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

Предлагаемый подход

  1. Учитывая, чтоConcurrentHashMap поставляется с собственным механизмом блокировки, вы можете прекратить использовать собственную блокировку Reentrant во время работы с ней.

  2. Обновите свой код, чтобы позволить получателям WriteLock иметь возможность работать:)

  3. Проверьте вашу версию Java, так как я никогда не доходил до заблокированного состояния при работе на Java 1.8.0_201

...