41 父类持有子类的引用多线程加载造成死锁问题

Posted 蓝风9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了41 父类持有子类的引用多线程加载造成死锁问题相关的知识,希望对你有一定的参考价值。

前言

之前在 某技术交流群 中看到了这样的一个问题  

遇到一个没想明白的问题。
一个类的静态字段是当前类的子类,多线程同时实例化该类和子类时,线程卡住。

改问题的作者将问题记录在了他的 blog 静态字段是当前类的子类,线程卡住 · 语雀 

呵呵 这应该是几周之前的问题了, 拿出来 遛一遛 

如果 您知道 类初始化 的一些流程的话, 那这个问题 应该还是比较简单 

然后 我们这里调试一下 这里的具体的情况  

以下截图, 调试流程基于 openjdk9 

测试用例

我这里在 作者原有的 case 上面做了一定的简化调整 

原有的 case 里面, 是有 三个类, 我这里调整成为了只有 父类, 子类 

大概率偶现, 如果没有出现 请多试几次 

package com.hx.test12;

/**
 * Test22DbWhereProblem
 *
 * @author Jerry.X.He <970655147@qq.com>
 * @version 1.0
 * @date 2021-07-19 10:52
 */
public class Test01DbWhereProblem 

    // Test22DbWhereProblem
//    遇到一个没想明白的问题。
//    一个类的静态字段是当前类的子类,多线程同时实例化该类和子类时,线程卡住。
//    https://www.yuque.com/buildup/java/tzemks
    // ThreadA 组线程, 获取了 DbWhere 的锁, 尝试获取 EmptiableWhere 的锁
    // ThreadB 获取了 EmptiableWhere 的锁, 尝试获取 DbWhere 的锁, 导致死锁
    public static void main(String[] args) 

        System.out.println("DbWhereTest4");
        for (int i = 0; i < 3; i++) 
            new Thread(() -> 
                new DbWhere();
            , "ThreadA").start();
            new Thread(() -> 
                new EmptiableWhere();
            , "ThreadB").start();
        

    

    private static class DbWhere 
        /** 没有查询条件的空Where **/
        public static final EmptiableWhere NONE = new EmptiableWhere();

        public DbWhere() 
            log(this.getClass(), "DbWhere<init>()");
        

        static void log(Class<?> clazz, String msg) 
            System.out.println(Thread.currentThread().getName() + ' ' + clazz.getSimpleName() + '-' + msg);
        
    

    /** 允许为空的查询条件 **/
    private static class EmptiableWhere extends DbWhere 
        public EmptiableWhere() 
            log(this.getClass(), "EmptiableWhere<init>()");
        
    


类初始化流程

我们这里着重关注的是 "The procedure for initializing C is then as follows" 下面的这一系列的流程 

这是规范的约束, 具体在 HotspotVM 里面的实现, 大致的流程是一样的, 但是有一部分的细节是有差异的 

诸如 step5 里面的 "Then, initialize each final static field of C with the constant value in its ConstantValue attribute (§4.7.2), in the order the fields appear in the ClassFile structure."

然后 您结合 类初始化的流程再思考一下 ? 

1. 初始化的流程中 ThreadA 持有了 DBWhere 的锁, 并处于 初始化中的状态, 调用 <clinit>, 然后尝试去初始化 EmptiableWhere

2. 初始化的流程中 ThreadB 持有了 EmptiableWhere 的锁, 并处于 初始化中的状态, 继承自 DBWhere, 然后尝试去初始化 DBWhere

3. ThreadA, ThreadB 都处于 初始化中 的状态, 然后都想获取对方已经持有的锁, 并且已经持有的锁不能放弃, 造成了 死锁 

流程调试 

当然这个 一调试 问题就出来了, 到底符不符合上面的大致的猜想呢 

1. 我们来看一下 ThreadA 的情况, 这里是在调用 DbWhere 的 <clinit>, 然后 继而触发了下一个类的 初始化 

2. 我们来看一下 ThreadA 的阻塞的情况,  DbWhere 的 <clinit> 导致了 EmptiableWhere 的初始化, 然后这里因为 EmptiableWhere 处于初始化中, 并且是被另外的一个线程在初始化, 因此这里 等待 EmptiableWhere 的初始化结束 

3. 我们来看一下 ThreadB 的情况, 这里正在加载的类是 EmptiableWhere, 这里来初始化 它的基类 DbWhere, 然后发现 DbWhere 处于初始化中, 并且是被另外的一个线程在初始化, 因此这里 等待 DbWhere 的初始化结束

4. 我们来看一下 ThreadB 的阻塞的情况,  发现 DbWhere 处于初始化中, 并且是被另外的一个线程在初始化, 因此这里 等待 DbWhere 的初始化结束 

5. 构成死锁的条件, 造成死锁 

相对来说还是比较简单 

解决方式 那就是看是否真的有必要 出现这种 类继承结构 和 组合关系, 来避免问题的出现 

完 

参考

5.5. Initialization

静态字段是当前类的子类,线程卡住

以上是关于41 父类持有子类的引用多线程加载造成死锁问题的主要内容,如果未能解决你的问题,请参考以下文章

java 多线程-死锁的产生以及解决方案

死锁问题

Java 死锁

Java多线程——死锁

如何解决多线程造成的数据库死锁

关于线程死锁问题