提问者:小点点

当使用在并发HashMap::compute()


太长别读——在我的应用程序中,许多线程在通过compute()方法将条目插入到ConCurrentHashMap时,在READ模式下抓取ReentantReadWriteLock,并在传递给compute()的lamdba完成后释放READ锁。有一个单独的线程在WRITE模式下抓取ReentantReadWriteLock并非常(非常)快速地释放它。当这一切发生时,并发HashMap正在调整大小(增长和收缩)。我遇到了一个挂起,我总是在堆栈跟踪中看到::的转移(),它在调整大小期间被调用。所有线程都被阻塞等待抓取MY ReentantReadWriteLock。复制器位于:https://github.com/rumpelstiltzkin/jdk_locking_bug

根据记录的行为,我做错了什么,还是这是一个JDK的bug?请注意,我不是在要求其他方法来实现我的应用程序。

详细信息:以下是关于为什么我的应用程序正在做它正在做的事情的一些上下文。再现器代码是一个简化版本来演示问题。

我的应用程序有一个直写缓存。条目被插入到缓存中,并带有插入时间的时间戳,单独的flusher-线程迭代缓存以查找在flusher-线程上次将条目持久化到磁盘之后创建的条目,即在last-flush-time之后。缓存只不过是一个并发HashMap。

现在,一个竞争是可能的,一个条目被构造成一个时间戳tX,当它被插入到并发HashMap中时,flusher-线程迭代缓存并且没有找到这个条目(它仍然被插入,所以在flusher-线程的Map::Iterator中还不可见),所以它不会持久化它,并且将last-flush-time颠簸到tY,使得tY

为了解决这个问题,使用新条目更新缓存的线程会在Lambda中以READ模式抓取一个ReentantReadWriteLock,该lambda::compute()方法中构造缓存条目,并且flusher-线程在抓取其last-flush-time时以WRITE模式抓取相同的ReentantReadWriteLock。这确保了当flusher-线程抓取时间戳时,所有对象在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进程已经Hibernate了几分钟(它使用一点点的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

共1个答案

匿名用户

注意:以下是观察结果、建议方法和向Oracle提交bug的建议列表。不是解决方案。

>

  • 并发映射具有内置的锁定机制,我们不需要自己获取一个

    原子*类在“单个”cpu周期内返回,因此在处理它们时无需获取锁

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

    并发映射的迭代器是弱一致的,即使插入完成,它们也不会看到您的更新。这是设计。

    我已经恢复了原子整数,因为我不想使用Holder(来自jax-ws),并且我无法重现您的线程阻塞。

    鉴于您在启动WriteLock获取线程之前启动了ReadLock获取线程。WriteLock获取线程将永远不会有机会运行,因为已经有一堆已经获取了读锁的线程。

    在释放ReadLock后在Cache#update方法中引入1秒的睡眠为WriteLock获取线程提供了运行的机会。

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

    一种。使用Holder进行lockCount可以让系统在短时间内爬行。

    湾。使用AnalyicInteger进行lockCount将寿命延长了几秒钟

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

    d。在控制台输出中将Id替换为当前线程的名称完全解决了问题。

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

    >

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

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

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