乐趣区

Java-类加载之匿名类和主类相互依赖问题

Qestion

/**
 * ClassInitializedOrder for : Java Classload Order Test
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初 </a>
 * @since 2019/7/20
 */
// CASE 1  
public class ClassInitializedOrder {
    private static boolean initialized = false;
    static {println("static 代码块执行。");
        Thread thread = new Thread(() -> initialized = true);
        thread.start();
        try {thread.join();
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

    public static void main(String[] args) {println("main 函数执行。");
        System.out.println("initialized =" + initialized);
    }

    private static void println(Object o){System.out.println(o);
    }
}

-------------------------------------------------------------------
// CASE 2    
public class ClassInitializedOrder {
    private static boolean initialized = false;
    static {println("static 代码块执行。");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {println("Runnable 代码块执行。");
                initialized = true;
            }
        });
        thread.start();
        try {thread.join();
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

    public static void main(String[] args) {println("main 函数执行。");
        System.out.println("initialized =" + initialized);
    }

    private static void println(Object o){System.out.println(o);
    }

Answer

  • A: initialized = true
  • B: initialized = false
  • C: 编译错误
  • D: 以上答案都是错的

Explain

程序执行的时候,App Classloader 会首先加载ClassInitializedOrder.class, 按照类的顺序依次执行。

private static boolean initialized = false;

CASE 1

我们都知道,static块会在类加载的时候初始化,那么下一步会执行到 Thread thread = new Thread(() -> initialized = true); 我们先来看一下当前行的字节码:

static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=3, locals=2, args_size=0
         0: iconst_0
         1: putstatic     #7                  // Field initialized:Z
         4: new           #11                 // class java/lang/Thread
         7: dup
         8: invokedynamic #12,  0             // InvokeDynamic #0:run:()Ljava/lang/Runnable;
        13: invokespecial #13                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
        16: astore_0
        17: aload_0
        18: invokevirtual #14                 // Method java/lang/Thread.start:()V
        21: aload_0
        22: invokevirtual #15                 // Method java/lang/Thread.join:()V
        25: goto          33
        28: astore_1
        29: aload_1
        30: invokevirtual #17                 // Method java/lang/InterruptedException.printStackTrace:()V
        33: return

分析 #12 可以看到当前行的处理需要 () 也就是改匿名类本身来处理,InvokeDynamic指令的在当前的执行又依赖于当前所处的主类,主类并没有执行结束,因此它需要等待主类执行结束,因此会在此停顿,如下:

CASE 2

继续查看字节码:

 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=4, locals=2, args_size=0
         0: iconst_0
         1: putstatic     #1                  // Field initialized:Z
         4: ldc           #14                 // String static 代码块执行。6: invokestatic  #2                  // Method println:(Ljava/lang/Object;)V
         9: new           #15                 // class java/lang/Thread
        12: dup
        13: new           #16                 // class com/sxzhongf/daily/question/july/ClassInitializedOrder$1
        16: dup
        17: invokespecial #17                 // Method com/sxzhongf/daily/question/july/ClassInitializedOrder$1."<init>":()V
        20: invokespecial #18                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
        23: astore_0
        24: aload_0
        25: invokevirtual #19                 // Method java/lang/Thread.start:()V
        28: aload_0
        29: invokevirtual #20                 // Method java/lang/Thread.join:()V
        32: goto          40
        35: astore_1
        36: aload_1
        37: invokevirtual #22                 // Method java/lang/InterruptedException.printStackTrace:()V
        40: return

查看#16, 我们可以看到这里变成了new #16 // class com/sxzhongf/daily/question/july/ClassInitializedOrder$1, 可以明显看到从之前的invokeDynamic 变成了 new 一个匿名类,那么它的结果呢?

依然还是 block. 我们来换一行代码试试?

public class ClassInitializedOrder {
    private static boolean initialized = false;
    static {println("static 代码块执行。");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {//println("Runnable 代码块执行。");
                System.out.println("Runnable 代码块执行。");
                //initialized = true;
            }
        });
        thread.start();
        try {thread.join();
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

我们看到我们只是修改了一行代码System.out.println("Runnable 代码块执行。");,那么结果呢?

执行成功的返回了。为什么?继续查看字节码

 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=4, locals=2, args_size=0
         0: iconst_0
         1: putstatic     #9                  // Field initialized:Z
         4: ldc           #14                 // String static 代码块执行。6: invokestatic  #3                  // Method println:(Ljava/lang/Object;)V
         9: new           #15                 // class java/lang/Thread
        12: dup
        13: new           #16                 // class com/sxzhongf/daily/question/july/ClassInitializedOrder$1
        16: dup
        17: invokespecial #17                 // Method com/sxzhongf/daily/question/july/ClassInitializedOrder$1."<init>":()V
        20: invokespecial #18                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
        23: astore_0
        24: aload_0
        25: invokevirtual #19                 // Method java/lang/Thread.start:()V
        28: aload_0
        29: invokevirtual #20                 // Method java/lang/Thread.join:()V
        32: goto          40
        35: astore_1
        36: aload_1
        37: invokevirtual #22                 // Method java/lang/InterruptedException.printStackTrace:()V
        40: return

查看 #16, 看到的还是new 了一个匿名类,和上一个是一样的,为什么就可以成功呢?这个在于当前匿名类中没有依赖主类的代码信息。不存在上下依赖,那么就不会出现相互等待的情况发生,当然也就不会出现 block。

那么就有朋友会问,为什么会相互等待呢? 这里和我们 join 就有关联了,我们来看一下它的实现代码。

public final synchronized void join(long millis)
    throws InterruptedException {long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {while (isAlive()) {wait(0);
            }
        } else {while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {break;}
                wait(delay);
                now = System.currentTimeMillis() - base;}
        }
    }

我们可以看到,首先它是 synchronized 关键词修饰的,那就说明它同时只能被一个线程访问,再往下看,我们能发现,join 的具体实现,其实就是 wait() 来实现,当子线程中的程序再等待 main 线程的实现类初始化完成的时候,又依赖了主线程中的某些元素对象。那么就会开始等待主线程初始化完成,这个时候,根据 classloader 加载类的执行顺序,在 #16 就会开始等待,那么主类无法初始化完成,造成相互等待现相。

Result

  • 匿名内置类的初始化不能依赖于外部类的初始化
  • lambda 表达式中 invokeDynamic 作为主类字节码的一部分,需要等待主类初始化完成才能开始执行

总之,在类的初始化阶段,不能出现内置类(匿名 /Lambda)和主类初始化中相互依赖的对象

退出移动版