在业务中曾有为不同的 key
的操作实现锁语义的需求,常见的实现为构建一个线程安全的 Map
实现然后为每个 key
创建对应的对象用于互斥锁,但是该实现存在内存泄漏的问题,即随着不同 key
不停地出现,该 Map
实例会越来越大,之前为每个 key
创建的对象一直驻留在内存中,即使 key
永远不再出现。比如 JDK 7 引入的支持并行类加载的特性 Multithreaded Custom Class Loaders in Java SE 7 中,引入了如下代码:
1 | // Maps class name to the corresponding lock object when the current |
以上代码在本文编写时依然存在于最新的 JDK 中,该实现会导致 parallelLockMap
持续增长。比如,我们编写以下代码:
1 | package me.tianshuang; |
当设置一个较低的 -Xmx
将会很快观察到 OOM 抛出,如:
1 | Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded |
关于该问题曾有用户提交过 bug,参见:JDK-8037342 parallelLockMap keeps growing - Java Bug System,但是并未被修复,而后 David Holmes 写了一篇博文 Parallel Classloading Revisited: Fully Concurrent Loading (David Holmes’ Weblog) 来阐述该问题并提供了几种潜在的修复方案,但是并未在 JDK 中得以实现,具体讨论可以参考:Proposal: Fully Concurrent ClassLoading,此处不再赘述。
在我们之前的工程中,也存在类似的代码。同样地,在长时间运行后,随着不同 key
的累积,导致了内存的不停增长,只不过在 OOM 前我们收到了监控告警,提前修复了该问题。我们使用 Striped
限制锁的个数来避免为每个 key
创建锁导致的内存持续上升的问题,当然,并发度不再是单个 key
级别,但是,在大部分场景下,Striped
已经能够满足需求。关于其介绍可以参考 Striped (Guava: Google Core Libraries for Java SNAPSHOT API),即我们可以在内存占用与并发度之间取得平衡,同时我们还可以使用弱引用、懒加载能进一步控制内存占用。Striped
的代码实现不算复杂,主要提供了两种实现,一种为基于数组的实现,一种为基于 Map
的实现,具体可参考 guava/Striped.java at v31.1 · google/guava · GitHub。
也有观点认为可以使用 String.intern()
的返回值作为锁对象,但是关于该用法存在争议。因为 String
对象实例是 JVM 范围内共享的,那么就存在与其他代码共用一把锁的风险,即使可以通过命名空间等措施来降低冲突的概率,但是风险依然存在。还有观点认为 String.intern()
的性能取决于 JVM 底层实现,存在不可控的因素,相关讨论可以参考:Synchronizing on String objects in Java - Stack Overflow。
Hive
在 Hive 的 HMSHandler 类中即使用了 Striped
对指定数据库的指定表进行加锁。
Reference
How to acquire a lock by a key - Stack Overflow
StripedExplained · google/guava Wiki · GitHub
Problems with padding · Issue #3195 · google/guava · GitHub