知识卡片
细粒度锁实现存在漏洞反而引发死锁:好的设计意图未必能正确落地
内容
Spark提供了spark.executor.userClassPathFirst这个配置项,本意是缓解用户自己的代码库和Spark系统代码库之间可能出现的类冲突问题。Hulu在实践中却发现,在大并发场景下加载相同的类时,有一定概率(在他们的场景下大约十分之一)会触发死锁。深入排查后发现问题出在Spark为此新增的ChildFirstURLClassLoader实现上——Java 7原生的ClassLoader提供了细粒度的类加载并发锁能力,能做到为每一个具体的类名单独分配一把锁(而不是所有类加载共用一把粗粒度的大锁),但要用上这种细粒度锁机制有一个前提条件:用户自己实现的ClassLoader必须在自身的静态初始化方法里,把自己正确注册到Java原生机制认可的体系中;Spark的ChildFirstURLClassLoader试图模仿Java原生ClassLoader的方式去实现自己的细粒度类加载锁,但因为实现上没有满足这个注册条件(它的初始化方式和Java原生要求的静态初始化方法之间存在类型和时机上的不完全匹配),这段试图实现细粒度锁的代码实际上根本没有达到预期效果,最终还是会退化到使用更粗粒度的ClassLoader级别锁,而在这种退化状态下,某些并发场景下就会触发死锁。Hulu最终的解决方法是直接去除这段没能正确实现预期效果的细粒度锁代码。这个案例给出了一条评估任何”为了优化并发性能而引入的精细化锁机制”的重要提醒:一段代码的设计意图(这里是”想实现细粒度锁来提升并发性能”)和它实际达成的效果之间,未必是一致的——即使开发者清楚地知道该往哪个方向优化、也确实动手实现了看起来对的方案,如果这个实现在细节上没有满足某个隐藏的前提条件(这里是Java ClassLoader细粒度锁机制要求的注册方式),最终的实际行为可能会悄悄退化回原来更粗粒度、甚至引发新问题(死锁)的状态,而这种退化往往不会在代码层面直接暴露出来,只有在特定并发场景下才会偶发触发——面对这类声称”已经优化过并发性能”的机制,如果在实际使用中遇到了诡异的并发问题(比如低概率死锁),有必要去审视这个优化机制本身是不是真的达成了它声称的效果,而不是想当然地信任它的设计意图。