TL;DR - 在我的应用程序中,许多线程在通过 compute() 方法将条目插入 ConcurrentHashMap 时以 READ 模式获取 ReentrantReadWriteLock,并在传递给 compute() 的 lamdba 完成后释放 READ 锁。有一个单独的线程在 WRITE 模式下获取 ReentrantReadWriteLock 并非常(非常)快速地释放它。虽然这一切都在发生,但 ConcurrentHashMap 正在调整大小(增长和缩小)。我遇到了挂起,我总是在堆栈跟踪中看到在调整大小期间调用的 ConcurrentHashMap::transfer()。所有线程都被阻塞等待获取我的 ReentrantReadWriteLock。转载者:https ://github.com/rumpelstiltzkin/jdk_locking_bug
根据记录的行为,我做错了什么,还是这是一个 JDK 错误?请注意,我不是要求其他方式来实现我的应用程序。
详细信息:这里有一些关于为什么我的应用程序正在做它正在做的事情的上下文。复制器代码是用于演示问题的精简版本。
我的应用程序有一个直写缓存。条目被插入到缓存中,并带有插入时间的时间戳,并且单独的刷新线程迭代缓存以查找在最后一次刷新线程将条目持久保存到磁盘之后创建的条目,即在 last-flush-time 之后。缓存只不过是一个 ConcurrentHashMap。
现在,可能会出现竞争,即使用时间戳 tX 构造条目,并且在将其插入 ConcurrentHashMap 时,刷新器线程迭代缓存并且找不到条目(它仍在插入,因此在刷新器中尚不可见-thread 的 Map::Iterator),因此它不会持久化它,并将最后一次刷新时间增加到 tY,使得 tY > tX。下次刷新线程迭代缓存时,它不会认为需要刷新 tX-timestamped 条目,我们将错过持久化它。最终 tX 将是一个非常旧的时间戳,缓存将永久删除它并丢失该更新。
为了解决这个问题,使用新条目更新缓存的线程在 lambda 中以 READ 模式获取 ReentrantReadWriteLock,该 lambda 在 ConcurrentHashMap::compute() 方法中构造缓存条目,并且刷新线程在 WRITE 模式下获取相同的 ReentrantReadWriteLock当抓住它的最后一次冲洗时间时。这确保了当刷新线程获取时间戳时,所有对象在 Map 中都是“可见的”,并且时间戳 <= 上次刷新时间。
在我的系统上复制:
$> 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 进程已经休眠了几分钟(它逐渐使用一点点 CPU 来进行可能的上下文切换以及什么不是 - 请参阅 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