Java 类初始化与this逃逸

前言

想记录一下对类初始化的理解,并且this逃逸也与类初始化有关,所以放到一起了。

类的初始化

先看一段代码,并想想它的运行结果是什么?

public class StaticTest {

    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    StaticTest() {
        System.out.println("3");
        System.out.println("a = " + a + ", b = " + b);
    }

    public static void staticFunction() {
        System.out.println("4");
    }

    int a = 10;

    static int b = 112;

}

运行结果:

2
3
a = 10, b = 0
1
4

是不是如你所想呢?
不管是不是也说说出现这个结果的步骤吧

静态域先于实例域初始化这个是大家都知道的,但是为什么上面的输出中看到实例域的代码反而先运行了呢?
关键代码:

static StaticTest st = new StaticTest();

静态域的第一行执行了构造方法,在执行main方法时,先是加载这个类,加载完毕后紧接着初始化,初始化则是初始化静态域,实例域是赋0值(不同的类型0值不同),但因为这里调用了构造方法,就开始执行实例化的流程,初始化实例域的顺序是按照代码在文件中出现的顺序来的同时又先于构造器(这是为什么等会说),所以先是输出2,然后执行a=10,输出3和"a=10, b = 0"但是为什么b=0呢,因为静态域也是按照先出现先执行的原则进行执行的,最后执行方法输出4。

通过字节码文件可以更清晰的看懂初始化流程。

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #1                  // Method staticFunction:()V
       3: return

  mytest.inittest.StaticTest();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method java/lang/Object."<init>":()V
       4: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #4                  // String 2
       9: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_0
      13: bipush        10
      15: putfield      #6                  // Field a:I
      18: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      21: ldc           #7                  // String 3
      23: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      26: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      29: new           #8                  // class java/lang/StringBuilder
      32: dup
      33: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
      36: ldc           #10                 // String a =
      38: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      41: aload_0
      42: getfield      #6                  // Field a:I
      45: invokevirtual #12                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      48: ldc           #13                 // String , b =
      50: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      53: getstatic     #14                 // Field b:I
      56: invokevirtual #12                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      59: invokevirtual #15                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      62: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      65: return

  public static void staticFunction();
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #16                 // String 4
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  static {};
    Code:
       0: new           #17                 // class mytest/inittest/StaticTest
       3: dup
       4: invokespecial #18                 // Method "<init>":()V
       7: putstatic     #19                 // Field st:Lmytest/inittest/StaticTest;
      10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      13: ldc           #20                 // String 1
      15: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      18: bipush        112
      20: putstatic     #14                 // Field b:I
      23: return

通过字节码可以清楚的看到,之前的类文件被分成了四个部分,分别是2个方法和构造函数和一个静态域,这也是之前说的实例域的代码会在构造器前执行同时按照出现的顺序排列,静态域也是一样,是根据字节码文件下的结论。毕竟字节码文件是严格按照虚拟机规范的产物。

从字节码文件看到static这部分,

  1. 调用构造函数,
  2. 打印1,
  3. 给b赋值

构造函数部分

  1. 打印2
  2. 给a赋值
  3. 打印a=10, b=0

new的时候就跳到构造函数了,也就是StaticTest(),这里把散落在构造函数外的代码块和成员赋值的代码都收集并放到构造函数原来代码的前面了(先于构造函数执行,并且如果在实例域调用构造函数是会出现栈溢出的)。所以实际上还是按照先静态再实例的初始化顺序执行的。


This逃逸

this逃逸说的是在初始化还未完全时将this所指向的地址传递出去,使得其它线程可以访问到未初始化完全的实例。也就是说它发生在并发场景下,想来也是,单线程大家都是顺序执行,哪来逃逸一说。

new不是原子操作可以说是其中一个原因,new操作分为下面三步:

  1. 分配内存空间
  2. 初始化对象
  3. 将声明的变量指向此实例地址

但是经过jvm的指令重排后三步变化了:

  1. 分配内存空间
  2. 将声明的变量指向实例地址
  3. 初始化对象

2和3的交换是导致this逸出的根本原因。
下面是代码示例:

public class ThisEscapeTest {

    private String status;
    private String message;

    public ThisEscapeTest(Notifier notifier) throws InterruptedException {
        status = "new";
        notifier.registerListener((Event event) -> {
            // 输出的结果并不是在构造函数中打印的,也就是说不是在主线程中打印的
            System.out.println("current thread: " + Thread.currentThread().getName());
            System.out.println("status: " + status);
            System.out.println("message: " + message);
        });
        // 模拟耗时操作,导致初始化结束前其它线程访问了此内存空间
        Thread.sleep(2000);
        message = "message";
    }

    public static void main(String[] args) throws InterruptedException {
        Notifier notifier = new Notifier();
        new Thread(() -> {
            try {
                // 这里发生某些事件,导致通知者通知监听者
                notifier.notifyListeners();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();
        new ThisEscapeTest(notifier);
    }

    // 在示例中没有实际用处,模拟正常情况的监听事件
    static class Event {
    }

    // 通知者,注册监听者和在出现事件时通知已注册的监听者
    static class Notifier {

        ArrayList<EvenListener> listeners = new ArrayList<>();

        synchronized void registerListener(EvenListener listener) {
            listeners.add(listener);
            this.notifyAll();
        }

        synchronized void notifyListeners() throws InterruptedException {
            while (listeners.size() <= 0) {
                this.wait();
            }
            // 输出结果是在这里打印的
            for (EvenListener listener : listeners) {
                listener.doSomething(new Event());
            }
        }

    }

    // 监听者接口
    interface EvenListener {

        void doSomething(Event event);

    }
}

输出结果:

current thread: A
status: new
message: null

message为null说明初始化未完全已被其它线程访问。

参考:
https://www.cnblogs.com/viscu/p/9790755.html

全部评论

相关推荐

不愿透露姓名的神秘牛友
06-21 11:33
昨天是学校最后一场招聘会,鼠鼠去参加了,全场只有一个招聘java的岗位,上来先做一份笔试题,做完后他拿张纸对答案,然后开始问简历上的问题,深圳小厂,6-8k(题目如下),后面还有两轮面试。然后我就在招聘现场逛呀逛,看到有公司招聘电商运营,给的比上年的小厂还多,鼠鼠就去了解了下,然后hr跟鼠鼠要了份简历,虽然我的简历上面全是求职Java开发相关的内容,但是hr还是鼓励我说没关系,她帮我把简历给老板看看,下周一会给我通知。招聘会结束后鼠鼠想了一段时间,也和朋友聊了聊,发现我可能是不太适合这个方向,然后就跟爸爸说回家了给我发条微信,我有些话想跟他说说。晚上爸爸到家了,跟我发了条微信,我立马跑出图书馆跟他打起了电话,这个通话长达一个小时,主要是跟爸爸坦白说我不想找这行了,是你的儿子太没用了,想试试其他行业。然后爸爸也跟我说了很多,说他从来没有希望我毕业后就赚大钱的想法,找不到就回家去,回家了再慢慢找,实在找不到就跟他干(帮别人装修房子,个体户),他也知道工作不好找,让我不要那么焦虑,然后就是聊一些家常琐事。对于后面的求职者呢我有点建议想提一下,就是如果招实习的时间或者秋招开始,而你的简历又很差的情况下,不要说等做好项目填充完简历之后再投,那样就太晚了,建议先把熟悉的项目写上简历,然后边投边面边完善,求职是一个人进步的过程,本来就比别人慢,等到一切都准备好后再投岂不是黄花菜都凉了。时间够的话还是建议敲一遍代码,因为那样能让你加深一下对项目的理解,上面那些说法只是针对时间不够的情况。当然,这些建议可能没啥用,因为我只是一个loser,这些全是建立在我理想的情况下,有没有用还需其他人现身说法。上篇帖子没想到学校被人认了出来,为了不丢脸只能匿名处理了。
KPLACE:找研发类或技术类,主要还是要1.多投 2.多做准备,很多方面都要做准备 3.要有心理准备,投累了就休息一两天,再继续,要相信自己能找到
投递58到家等公司7个岗位
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务