0

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
4

1 回答 1

0

注意:下面总结了观察列表、建议的方法和向 Oracle 提交错误的建议。不是解决方案。

观察

  1. Concurrent Maps 有其内置的锁定机制,我们不需要自己获取

  2. Atomic* 类在“单个”cpu 周期内返回,因此在处理它们时不需要获取锁

  3. 在 Cache.java 中,您正在获取(您自己的)ReadLock 用于更新缓存(第 34 行),以及(您自己的)WriteLock 用于从映射中读取(第 58 行),并且在您实际删除映射时不获取任何锁(第 71 行)。

  4. Concurrent Maps 的迭代器是弱一致的,即使插入完成,它们也不会看到你的更新。这是设计使然。

  5. 我已经恢复了 AtomicInteger,因为我不想使用 Holder(来自 jax-ws),而且我无法重现你的线程阻塞。

  6. 假设您在开始 WriteLock 获取线程之前启动 ReadLock 获取线程。WriteLock 获取线程永远不会有机会运行,因为已经有一堆已经获取了 Read Locks 的线程。

  7. 在释放 ReadLock 后,在 Cache#update 方法中引入 1 秒的睡眠,让 WriteLock 获取线程有机会运行。

  8. 我已恢复我的更新并且能够重现您的问题。但我确实看到了一个模式。

    一个。使用 Holder for lockCount 可以让系统立即爬网。

    湾。将 AtomicInteger 用于 lockCount 将寿命延长了几秒钟

    C。引入关于获取和释放具有 runnable id 的锁的控制台语句将生命周期延长了一两分钟。

    d。在控制台输出中用当前线程的名称替换 Id 已经完全解决了这个问题。

有了这个,它显然看起来像一个时间问题,因为读取器和写入器都在等待获取各自的锁,从而导致死锁,并且由于额外的语句引入的延迟减少了这种情况的可能性,因此在获取 readlock 和 writelock 时发生了竞争。

建议的方法

  1. 鉴于 ConcurrentHashMap 带有自己的锁定机制,您可以在处理它时停止使用自己的可重入锁。

  2. 更新您的代码以允许 WriteLock 获取者有机会运行:)

  3. 检查您的 Java 版本,因为在 Java 1.8.0_201 上运行时我从未进入阻塞状态

于 2019-10-23T02:03:55.437 回答