TL;DR. В моем приложении многие потоки захватывают ReentrantReadWriteLock в режиме READ, пока они вставляют записи в ConcurrentHashMap с помощью метода calculate(), и освобождают блокировку READ после завершения lamdba, переданного в calculate(). Есть отдельный поток, который захватывает ReentrantReadWriteLock в режиме WRITE и очень (очень) быстро его освобождает. Пока все это происходит, ConcurrentHashMap изменяет размер (растет и сжимается). Я сталкиваюсь с зависанием и всегда вижу ConcurrentHashMap::transfer(), который вызывается при изменении размера, в трассировках стека. Все потоки заблокированы, ожидая захвата MY ReentrantReadWriteLock. Источник: https://github.com/rumpelstiltzkin/jdk_locking_bug
Я делаю что-то не так в соответствии с задокументированным поведением или это ошибка JDK? Обратите внимание, что я НЕ прошу других способов реализации моего приложения.
Подробности: вот некоторый контекст, объясняющий, почему мое приложение делает то, что делает. Код воспроизводящего устройства представляет собой урезанную версию для демонстрации проблемы.
В моем приложении есть кеш со сквозной записью. Записи вставляются в кеш с отметкой времени, когда они были вставлены, и отдельный поток очистки выполняет итерацию кеша, чтобы найти записи, которые были созданы после последнего сохранения записей потоком очистки на диске, т.е. после времени последней очистки. Кэш — это не что иное, как ConcurrentHashMap.
Теперь возможна гонка, при которой запись создается с временной меткой tX, и пока она вставляется в ConcurrentHashMap, поток флешера перебирает кеш и не находит запись (она все еще вставляется так что он еще не виден в Map::Iterator потока-очистителя), и поэтому он не сохраняет его и увеличивает время последней очистки до tY, так что tY > tX. В следующий раз, когда поток-очиститель выполняет итерацию кеша, он не будет считать запись с отметкой времени tX требующей очистки, и мы пропустим ее сохранение. В конце концов tX станет очень старой отметкой времени, и кеш удалит ее безвозвратно, потеряв это обновление.
Чтобы обойти эту проблему, потоки, обновляющие кеш новыми записями, захватывают ReentrantReadWriteLock в режиме READ внутри лямбда-выражения, создающего запись в кеше внутри метода ConcurrentHashMap::compute(), а поток-очиститель захватывает тот же ReentrantReadWriteLock в режиме WRITE. при захвате времени последнего сброса. Это гарантирует, что когда поток очистки получает отметку времени, все объекты будут «видимы» на карте и будут иметь отметку времени ‹= время последней очистки.
Воспроизведение в моей системе:
$> 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 для получения дополнительных объяснений, почему это происходит)
$> 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