java this引用逃逸详解

1、什么是This逃逸?

  在构造器构造还未彻底完成前(即实例初始化阶段还未完成),将自身this引用向外抛出并被其他线程复制(访问)了该引用,可能会问到该还未被初始化的变量,甚至可能会造成更大严重的问题。

  废话不多说,看一下代码

 /**
  * 模拟this逃逸
  * @author Lijian
  *
  */
 public class ThisEscape {
   //final常量会保证在构造器内完成初始化(但是仅限于未发生this逃逸的情况下,具体可以看多线程对final保证可见性的实现)
   final int i;
   //尽管实例变量有初始值,但是还实例化完成
   int j = 0;
   static ThisEscape obj;
   public ThisEscape() {
     i=1;
     j=1;
     //将this逃逸抛出给线程B
     obj = this;
   }
   public static void main(String[] args) {
     //线程A:模拟构造器中this逃逸,将未构造完全对象引用抛出
     /*Thread threadA = new Thread(new Runnable() {
       @Override
       public void run() {
         //obj = new ThisEscape();
       }
     });*/
     //线程B:读取对象引用,访问i/j变量
     Thread threadB = new Thread(new Runnable() {
       @Override
       public void run() {
               //可能会发生初始化失败的情况解释:实例变量i的初始化被重排序到构造器外,此时1还未被初始化
         ThisEscape objB = obj;
         try {
           System.out.println(objB.j);
         } catch (NullPointerException e) {
           System.out.println("发生空指针错误:普通变量j未被初始化");
         }
         try {
           System.out.println(objB.i);
         } catch (NullPointerException e) {
           System.out.println("发生空指针错误:final变量i未被初始化");
         }
       }
     });
       //threadA.start();
       threadB.start();
   }
 }

输出结果:这说明ThisEscape还未完成实例化,构造还未彻底结束。

发生空指针错误:普通变量j未被初始化
发生空指针错误:final变量i未被初始化

另一种情况是利用线程A模拟this逃逸,但不一定会发生,线程A模拟构造器正在构造...而线程B尝试访问变量,这是因为

(1)由于JVM的指令重排序存在,实例变量i的初始化被安排到构造器外(final可见性保证是final变量规定在构造器中完成的);

(2)类似于this逃逸,线程A中构造器构造还未完全完成。

所以尝试多次输出(相信我一定会发生的,只是概率相对低),也会发生类似this引用逃逸的情况。

/**
 * 模拟this逃逸
 * @author Lijian
 *
 */
public class ThisEscape {
  //final常量会保证在构造器内完成初始化(但是仅限于未发送this逃逸的情况下)
  final int i;
  //尽管实例变量有初始值,但是还实例化完成
  int j = 0;
  static ThisEscape obj;
  public ThisEscape() {
    i=1;
    j=1;
    //obj = this ;
  }
  public static void main(String[] args) {
    //线程A:模拟构造器中this逃逸,将未构造完全对象引用抛出
    Thread threadA = new Thread(new Runnable() {
      @Override
      public void run() {
        //构造初始化中...线程B可能获取到还未被初始化完成的变量
        //类似于this逃逸,但并不定发生
        obj = new ThisEscape();
      }
    });
    //线程B:读取对象引用,访问i/j变量
    Thread threadB = new Thread(new Runnable() {
      @Override
      public void run() {
        //可能会发生初始化失败的情况解释:实例变量i的初始化被重排序到构造器外,此时1还未被初始化
        ThisEscape objB = obj;
        try {
          System.out.println(objB.j);
        } catch (NullPointerException e) {
          System.out.println("发生空指针错误:普通变量j未被初始化");
        }
        try {
          System.out.println(objB.i);
        } catch (NullPointerException e) {
          System.out.println("发生空指针错误:final变量i未被初始化");
        }
      }
    });
      threadA.start();
      threadB.start();
  }
}

2、什么情况下会This逃逸?

(1)在构造器中很明显地抛出this引用提供其他线程使用(如上述的明显将this抛出)。

(2)在构造器中内部类使用外部类情况:内部类访问外部类是没有任何条件的,也不要任何代价,也就造成了当外部类还未初始化完成的时候,内部类就尝试获取为初始化完成的变量

  • 在构造器中启动线程:启动的线程任务是内部类,在内部类中xxx.this访问了外部类实例,就会发生访问到还未初始化完成的变量
  • 在构造器中注册事件,这是因为在构造器中监听事件是有回调函数(可能访问了操作了实例变量),而事件监听一般都是异步的。在还未初始化完成之前就可能发生回调访问了未初始化的变量。

在构造器中启动线程代码实现:

/**
 * 模拟this逃逸2:构造器中启动线程
 * @author Lijian
 *
 */
public class ThisEscape2 {
  final int i;
  int j;
  public ThisEscape2() {
    i = 1;
    j = 1;
    new Thread(new RunablTest()).start();
  }
  //内部类实现Runnable:引用外部类
  private class RunablTest implements Runnable{
    @Override
    public void run() {
      try {
        System.out.println(ThisEscape2.this.j);
      } catch (NullPointerException e) {
        System.out.println("发生空指针错误:普通变量j未被初始化");
      }
      try {
        System.out.println(ThisEscape2.this.i);
      } catch (NullPointerException e) {
        System.out.println("发生空指针错误:final变量i未被初始化");
      }
    }

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

构造器中注册事件,引用网上的一段伪代码将以解释:

public class ThisEscape3 {
  private final int var;

  public ThisEscape3(EventSource source) {     //注册事件,会一直监听,当发生事件e时,会执行回调函数doSomething
    source.registerListener(       //匿名内部类实现
      new EventListener() {
        public void onEvent(Event e) {            //此时ThisEscape3可能还未初始化完成,var可能还未被赋值,自然就发生严重错误
          doSomething(e);
        }
      }
    );
    var = 10;
  }
  // 在回调函数中访问变量
  int doSomething(Event e) {
    return var;
  }
}

3、怎样避免This逃逸?
  (1)单独编写一个启动线程的方法,不要在构造器中启动线程,尝试在外部启动。

...
private Thread t;
public ThisEscape2() {
  t = new Thread(new EscapeRunnable());
}

public void initStart() {
  t.start();
}
...

  (2)将事件监听放置于构造器外,比如new Object()的时候就启动事件监听,但是在构造器内不能使用事件监听,那可以在static{}中加事件监听,这样就跟构造器解耦了

static{
  source.registerListener(
      new EventListener() {
        public void onEvent(Event e) {
          doSomething(e);
        }
      }
    );
    var = 10;
  }
}

4、总结

  this引用逃逸问题实则是Java多线程编程中需要注意的问题,引起逃逸的原因无非就是在多线程的编程中“滥用”引用(往往涉及构造器中显式或隐式地滥用this引用),在使用到this引用的时候需要特别注意!

  同时这会涉及到:final的内存语义,即final域禁止重排序问题(2020.11.22增加),包括写final域与读final域重排序两个规则(参考资料《Java并发编程的艺术》)

以上就是java this引用逃逸详解的详细内容,更多关于java this引用逃逸的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解Java基础篇--面向对象1(构造方法,static、this关键字)

    面向对象,面向过程的区别.拿下五子棋来说: 面向过程分析: 开始游戏 黑棋先走 绘制画面 判断输赢 轮到白棋 绘制画面 判断输赢 返回步骤2 输出结果 面向对象分析: 黑白双方,双方行为是一模一样的 棋盘系统,负责绘制画面 规则系统,判断犯规.输赢 传统的面向过程编程是思考问题的解决步骤,这种思维方式适用于问题规模较小时.可是当问题规模大,要求程序有更好的可扩展性,能更快速地查错时面向对象设计思想就能体现出其优势.面向对象更接近人类地自然思维方式,将现实世界中的事物抽象为对象和对象的方法. 面向

  • 浅谈Java中的this作为返回值时返回的是什么

    有时会遇到this作为返回值的情况,那么此时返回的到底是什么呢? 返回的是调用this所处方法的那个对象的引用,读起来有点绕口哈,有没有想起小学语文分析句子成份的试题,哈哈. 一点点分析的话,主干是"返回的是引用": 什么引用呢?"那个对象的引用": 哪个对象呢?"调用方法的那个对象": 调用的哪个方法呢?"调用的是this所位于的方法":这样就清楚了. 再总结一下就是,this作为返回值时,返回的是调用某方法的对象的引用,这

  • Java中super和this的用法详解

    super 用法 super关键字用来访问父类内容,具体用法可分为三种: 1.子类的成员方法访问父类的成员变量 public class Animal { public int age = 10; } class Dog extends Animal { public int age = 5; public void showAge() { System.out.println(super.age); } } 2.子类的成员方法访问父类的成员方法 public class Animal { pu

  • Java 中This用法的实例详解

     Java 中This用法的实例详解 用类名定义一个变量的时候,定义的只是一个引用,外面可以通过这个引用来访问这个类里面的属性和方法. 那们类里面是够也应该有一个引用来访问自己的属性和方法纳? 呵呵,Java提供了一个很好的东西,就是 this 对象,它可以在类里面来引用这个类的属性和方法.先来个简单的例子: public class ThisDemo { String name="Mick"; public void print(String name){ System.out.pr

  • java使用this调用构造函数的实现方法示例

    本文实例讲述了java使用this调用构造函数的实现方法.分享给大家供大家参考,具体如下: 一 点睛 如果在程序中想用某一个构造函数调用另一个构造函数,也可以用this来实现. 二 实战 1 代码 class Person { String name; int age; public Person() { System.out.println("1. public Person()"); } public Person(String name, int age) { // 调用本类中无

  • java中this的n种使用方法

    this可能是几乎所有有一点面向对象思想的语言都会引用到的变量,java自然不例外.只是,this有多少种用法,我也不知道了,让我们来see see. 由简入奢! 易. 来个例子说明下: public class DebugerTest { public static void main(String[] args) { UserExample samp1 = new UserExample("amy"); System.out.println("who are u? &qu

  • java中this与super关键字的使用方法

    java中this与super关键字的使用方法 这几天看到类在继承时会用到this和super,这里就做了一点总结,与各位共同交流,有错误请各位指正~ this this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针. this的用法在java中大体可以分为3种: 1.普通的直接引用 这种就不用讲了,this相当于是指向当前对象本身. 2.形参与成员名字重名,用this来区分: class Person { private int age = 10; public Perso

  • Java并发编程this逃逸问题总结

    this逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误, 因此应该避免this逃逸的发生. this逃逸经常发生在构造函数中启动线程或注册监听器时, 如: public class ThisEscape { public ThisEscape() { new Thread(new EscapeRunnable()).start(); // ... } private class EscapeRunnable implements Run

  • Java this 关键字的使用方法详解

    Java this 关键字的使用方法详解 构造方法中的this关键字 构造方法是一个类的对象在通过new关键字创建时自动调用的,在程序中不能向调用其他方法一样通过方法名(也就是类名)来调用.但如果一个类有多个构造方法,可以在一个构造方法中通过this(paras-)来调用其他的构造方法. 使用this来调用其他构造方法有如下几个约束. 1) 只能在构造方法中通过this来调用其他构造方法,普通方法中不能使用. 2) 不能通过this递归调用构造方法,即不能在一个构造方法中通过this直接或间接调

  • Java this super代码实例及使用方法总结

    这几天看到类在继承时会用到this和super,这里就做了一点总结,与各位共同交流,有错误请各位指正~ 一.this this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针. this的用法在java中大体可以分为3种: 1.普通的直接引用 这种就不用讲了,this相当于是指向当前对象本身. 2.形参与成员名字重名,用this来区分: package com.demo; public class Person { private int age = 10; public Pe

  • Java中this,static,final,const用法详解

    一.this 用类名定义一个变量的时候,定义的应该只是一个引用,外面可以通过这个引用来访问这个类里面的属性和方法,那们类里面是够也应该有一个引用来访问自己的属性和方法纳?JAVA提供了一个很好的东西,就是 this 对象,它可以在类里面来引用这个类的属性和方法. Java关键字this只能用于方法方法体内.当一个对象创建后,Java虚拟机(JVM)就会给这个对象分配一个引用自身的指针,这个指针的名字就是 this.因此,this只能在类中的非静态方法中使用,静态方法和静态的代码块中绝对不能出现t

随机推荐