zoukankan      html  css  js  c++  java
  • this引用逃逸

    1、什么是This逃逸?

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

      废话不多说,看一下代码

     1 /**
     2  * 模拟this逃逸
     3  * @author Lijian
     4  *
     5  */
     6 public class ThisEscape {
     7     //final常量会保证在构造器内完成初始化(但是仅限于未发生this逃逸的情况下,具体可以看多线程对final保证可见性的实现)
     8     final int i;
     9     //尽管实例变量有初始值,但是还实例化完成
    10     int j = 0;
    11     static ThisEscape obj;
    12     public ThisEscape() {
    13         i=1;
    14         j=1;
    15         //将this逃逸抛出给线程B
    16         obj = new ThisEscape();
    17     }
    18     public static void main(String[] args) {
    19         //线程A:模拟构造器中this逃逸,将未构造完全对象引用抛出
    20         /*Thread threadA = new Thread(new Runnable() {
    21             @Override
    22             public void run() {
    23                 //obj = new ThisEscape();
    24             }
    25         });*/
    26         //线程B:读取对象引用,访问i/j变量
    27         Thread threadB = new Thread(new Runnable() {
    28             @Override
    29             public void run() {
    30             31                 //可能会发生初始化失败的情况解释:实例变量i的初始化被重排序到构造器外,此时1还未被初始化
    32                 ThisEscape objB = obj;
    33                 try {
    34                     System.out.println(objB.j);
    35                 } catch (NullPointerException e) {
    36                     System.out.println("发生空指针错误:普通变量j未被初始化");
    37                 }
    38                 try {
    39                     System.out.println(objB.i);
    40                 } catch (NullPointerException e) {
    41                     System.out.println("发生空指针错误:final变量i未被初始化");
    42                 }
    43             }
    44         });
    45             //threadA.start();
    46             threadB.start();
    47     }
    48 }

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

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

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

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

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

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

     1 /**
     2  * 模拟this逃逸
     3  * @author Lijian
     4  *
     5  */
     6 public class ThisEscape {
     7     //final常量会保证在构造器内完成初始化(但是仅限于未发送this逃逸的情况下)
     8     final int i;
     9     //尽管实例变量有初始值,但是还实例化完成
    10     int j = 0;
    11     static ThisEscape obj;
    12     public ThisEscape() {
    13         i=1;
    14         j=1;
    15         //obj = new ThisEscape();
    16     }
    17     public static void main(String[] args) {
    18         //线程A:模拟构造器中this逃逸,将未构造完全对象引用抛出
    19         Thread threadA = new Thread(new Runnable() {
    20             @Override
    21             public void run() {
    22                 //构造初始化中...线程B可能获取到还未被初始化完成的变量
    23                 //类似于this逃逸,但并不定发生
    24                 obj = new ThisEscape();
    25             }
    26         });
    27         //线程B:读取对象引用,访问i/j变量
    28         Thread threadB = new Thread(new Runnable() {
    29             @Override
    30             public void run() {
    31                 //可能会发生初始化失败的情况解释:实例变量i的初始化被重排序到构造器外,此时1还未被初始化
    32                 ThisEscape objB = obj;
    33                 try {
    34                     System.out.println(objB.j);
    35                 } catch (NullPointerException e) {
    36                     System.out.println("发生空指针错误:普通变量j未被初始化");
    37                 }
    38                 try {
    39                     System.out.println(objB.i);
    40                 } catch (NullPointerException e) {
    41                     System.out.println("发生空指针错误:final变量i未被初始化");
    42                 }
    43             }
    44         });
    45             threadA.start();
    46             threadB.start();
    47     }
    48 }

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

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

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

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

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

     1 /**
     2  * 模拟this逃逸2:构造器中启动线程
     3  * @author Lijian
     4  *
     5  */
     6 public class ThisEscape2 {
     7     final int i;
     8     int j;
     9     public ThisEscape2() {
    10         i = 1;
    11         j = 1;
    12         new Thread(new RunablTest()).start();
    13     }
    14     //内部类实现Runnable:引用外部类
    15     private class RunablTest implements Runnable{
    16         @Override
    17         public void run() {
    18             try {
    19                 System.out.println(ThisEscape2.this.j);
    20             } catch (NullPointerException e) {
    21                 System.out.println("发生空指针错误:普通变量j未被初始化");
    22             }
    23             try {
    24                 System.out.println(ThisEscape2.this.i);
    25             } catch (NullPointerException e) {
    26                 System.out.println("发生空指针错误:final变量i未被初始化");
    27             }
    28         }
    29         
    30     }
    31     public static void main(String[] args) {
    32         new ThisEscape2();
    33     }
    34 }

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

    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引用的时候需要特别注意!

  • 相关阅读:
    结构和联合
    字符串、字符和字节
    数组
    函数
    指针
    操作符和表达式
    语句
    数据
    TinyXML2 使用
    是否忘记了向源中添加“#include "StdAfx.h"”?
  • 原文地址:https://www.cnblogs.com/jian0110/p/9369096.html
Copyright © 2011-2022 走看看