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