1,this引用逃逸

并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了。
这是危及到线程安全的,因为其他线程有可能通过这个逸出的引用访问到“初始化了一半”的对象(partially-constructed object)。
这样就会出现某些线程中看到该对象的状态是没初始化完的状态,而在另外一些线程看到的却是已经初始化完的状态,
这种不一致性是不确定的,程序也会因此而产生一些无法预知的并发错误。

补充:内部的特性:

内部类、匿名内部类都可以访问外部类的对象的域,为什么会这样,
实际上是因为内部类构造的时候,会把外部类的对象this隐式的作为一个参数传递给内部类的构造方法,这个工作是编译器做的,
所以下面例子里的匿名内部类在构造ThisEscape时就把ThisEscape创建的对象隐式的传给匿名内部类了。

1,1,this引用逸出是如何产生的

正如代码清单1所示,ThisEscape在构造函数中引入了一个内部类EventListener,而内部类会自动的持有其外部类(这里是ThisEscape)的this引用。
source.registerListener会将内部类发布出去,从而ThisEscape.this引用也随着内部类被发布了出去。
但此时ThisEscape对象还没有构造完成,id已被赋值为1,但name还没被赋值,仍然为null。

ps:简单来说就是,

在一个类的构造器创建了一个内部类(内部类本身是拥有对外部类的所有成员的访问权的),此时外部类的成员变量还没初始化完成。
但是,同时这个内部类被其他线程获取到,并且调用了内部类可以访问到外部类还没来得及初始化的成员变量的方法。

代码清单1 this引用逸出示例

public class ThisEscape {
public final int id;
public final String name;
public ThisEscape(EventSource<EventListener> source) {
id = 1;
source.registerListener(new EventListener() { //内部类是可以直接访问外部类的成员变量的(外部类引用this被内部类获取了)
public void onEvent(Object obj) {
System.out.println("id: "+ThisEscape.this.id);
System.out.println("name: "+ThisEscape.this.name);
}
});
name = "flysqrlboy";
} }

代码清单2 EventSource类:

public class EventSource<T> {
private final List<T> eventListeners ;
public EventSource() {
eventListeners = new ArrayList<T>() ;
} public synchronized void registerListener(T eventListener) { //数组持有传入对象的引用
this.eventListeners.add(eventListener);
this.notifyAll();
} public synchronized List<T> retrieveListeners() throws InterruptedException { //获取持有对象引用的数组
List<T> dest = null;
if(eventListeners.size() <= 0 ) {
this.wait();
}
dest = new ArrayList<T>(eventListeners.size()); //这里为什么要创建新数组,好处在哪里
dest.addAll(eventListeners);
return dest;
}
}

把内部类对象发布出去的source.registerListener语句没什么特殊的(发布其实就是让别的类有机会持有这个内部类的引用),

从代码清单2可发现,registerListener方法只是往list中添加一个EventListener元素而已。

这样,其他持有EventSource对象的线程从而持有EventListener对象,便可以访问ThisEscape的内部状态了(id和name)。

代码清单3中的ListenerRunnable 就是这样的线程:

public class ListenerRunnable implements Runnable {  

      private EventSource<EventListener> source;
public ListenerRunnable(EventSource<EventListener> source) {
this.source = source;
}
public void run() {
List<EventListener> listeners = null; try {
listeners = this.source.retrieveListeners();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for(EventListener listener : listeners) {
listener.onEvent(new Object()); //执行内部类获取外部类的成员变量的方法
}
}
}

只要线程得到持有内部类引用的数组,就可以使用内部类获取外部类的有可能未初始化的成员变量。

代码清单4 ThisEscapeTest

public class ThisEscapeTest {  

      public static void main(String[] args) {
EventSource<EventListener> source = new EventSource<EventListener>();
ListenerRunnable listRun = new ListenerRunnable(source);
Thread thread = new Thread(listRun);
thread.start();
ThisEscape escape1 = new ThisEscape(source);
}
}

启动了一个ListenerRunnable 线程,用于监视ThisEscape的内部状态。

主线程紧接着调用ThisEscape的构造函数,新建一个ThisEscape对象。

在ThisEscape构造函数中,如果在source.registerListener语句之后,name="flysqrlboy"赋值语句之前正好发生上下文切换,

ListenerRunnable 线程就有可能看到了还没初始化完的ThisEscape对象,即id为1,但是name仍然为null!

1,2,另外一种就是在在构造函数中启动新的线程的时候,容易发生This逃逸。代码如下:

public class ThreadThisEscape {    
//成员变量xxx
public ThisEscape() {
new Thread(new EscapeRunnable()).start(); //使用未初始化的成员变量
// 初始化成员变量
} private class EscapeRunnable implements Runnable {
@Override
public void run() {
//使用成员变量
// ThreadThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸
}
}
}

1,3,如何避免this引用逸出

导致的this引用逸出需要满足两个条件:

一个是在构造函数中创建内部类(EventListener),
另一个是在构造函数中就把这个内部类给发布了出去(source.registerListener)。

因此,我们要防止这一类this引用逸出的方法就是避免让这两个条件同时出现。

也就是说,如果要在构造函数中创建内部类,那么就不能在构造函数中把他发布了,应该在构造函数外发布,即等构造函数执行完初始化工作,再发布内部类。

正如如下所示,使用一个私有的构造函数进行初始化和一个公共的工厂方法进行发布。

public class ThisSafe {  

      public final int id;
public final String name;
private final EventListener listener; private ThisSafe() {
id = 1;
listener = new EventListener(){
public void onEvent(Object obj) {
System.out.println("id: "+ThisSafe.this.id);
System.out.println("name: "+ThisSafe.this.name);
}
};
name = "flysqrlboy";
} public static ThisSafe getInstance(EventSource<EventListener> source) {
ThisSafe safe = new ThisSafe(); //先初始化
source.registerListener(safe.listener); //发布内部类
return safe;
}

2,联想到构造器没有初始化完成就调用方法的情况。

(构造器是可以调用方法初始化变量的)

在父类构造函数内部调用具有多态行为的函数将导致无法预测的结果,因为此时子类对象还没初始化。

class Glyph {
void draw() { //没有执行
System.out.println("Glyph.draw()");
}
Glyph() { //3,默认调用
System.out.println("Glyph() before draw()");
draw(); //父类构造器作为子类构造器执行前的默认执行,此时父构造器内执行的方法是子类的重写方法
System.out.println("Glyph() after draw()");
}
} class RoundGlyph extends Glyph {
private int radius = 1; //5,初始化变量 RoundGlyph(int r) {//2,首先调用父类构造器(并且默认是无参构造器)
radius = r; //6,赋值执行
System.out.println("RoundGlyph.RoundGlyph(). radius = " + radius);
} void draw() { //4,在父构造器被调用,此时该类(子类)还没被初始化,所以实例变量的值为默认值。
System.out.println("RoundGlyph.draw(). radius = " + radius);
}
} public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);//1,首先执行
}}

输出:

Glyph() before draw()
RoundGlyph.draw(). radius = 0 //未被初始化
Glyph() after draw()
RoundGlyph.RoundGlyph(). radius = 5

为什么会这样输出?这就要明确掌握Java中构造函数的调用顺序

(1)在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制0;
(2)调用基类构造函数。从根开始递归下去,因为多态性此时调用子类覆盖后的draw()方法(要在调用RoundGlyph构造函数之前调用),
由于步骤1的缘故,我们此时会发现radius的值为0;
(3)按声明顺序调用成员的初始化方法;
(4)最后调用子类的构造函数。

3,子类何时调用父类构造器

子类总是会调用父类构造器,之所以需要调用父类的构造方法是因为在父类中,可能存在私有属性需要在其构造方法内初始化;

调用的情况情况:

1,默认情况,子类总是会调用父类默认无参构造器。
2,在子类构造器中指定需要调用的父类构造器(有/无参都可以),并且必须在子类的构造器中的第一行位置。
3,子类存在多个构造器,如果嵌套使用:(PS:编译期会合并其中的嵌套构造器)
3,1,合并后,默认也是调用父类的无参构造器。 
3,2,子类指定父类构造器,这时指定的父类构造器逻辑上在子类构造器的首行就好,因为会合并。

ps:19行中嵌套21行的构造器,经过编译期合并处理,父构造器仍然是在第一行中。

补充:

1,如果没有自定义构造器,编译期会默认为类添加无参构造器。
2,构造器的执行并不会创建对象,只有new+构造器的组合语句,才表示创建对象。

this引用逃逸(使用内部类获取未外部类未初始化完的变量),多态父类使用子类未初始化完的变量的更多相关文章

  1. 牛客网Java刷题知识点之什么是内部类、为什么要使用内部类、内部类如何使用外部类的属性和方法、成员内部类、局部内部类、静态内部类、匿名内部类

    不多说,直接上干货! 可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一个非常有用的特性但又比较难理解使用的特性(鄙人到现在都没有怎么使用过内部类,对内部类也只是略知一二). 内部类 ...

  2. 深入理解Java中为什么内部类可以访问外部类的成员

    内部类简介 虽然Java是一门相对比较简单的编程语言,但是对于初学者, 还是有很多东西感觉云里雾里, 理解的不是很清晰.内部类就是一个经常让初学者感到迷惑的特性. 即使现在我自认为Java学的不错了, ...

  3. Android(java)学习笔记150:为什么局部内部类只能访问外部类中的 final型的常量

    为什么匿名内部类参数必须为final类型: 1)  从程序设计语言的理论上:局部内部类(即:定义在方法中的内部类),由于本身就是在方法内部(可出现在形式参数定义处或者方法体处),因而访问方法中的局部变 ...

  4. Java中内部类揭秘(一):外部类与非静态内部类的”相互可见性“

               声明:本博客为原创博客.未经同意,不得转载.原文链接为 http://blog.csdn.net/bettarwang/article/details/27012421.     ...

  5. Android(java)学习笔记93:为什么局部内部类只能访问外部类中的 final型的常量

    为什么匿名内部类参数必须为final类型: 1)  从程序设计语言的理论上:局部内部类(即:定义在方法中的内部类),由于本身就是在方法内部(可出现在形式参数定义处或者方法体处),因而访问方法中的局部变 ...

  6. 141、Java内部类之实例化外部类对象

    01. 代码如下: package TIANPAN; class Outer { // 外部类 private static String msg = "Hello World !" ...

  7. java——多线程——内部类共享同一个外部类对象的成员变量

    public class Shop { public static void main(String[] args) { Outer o=new Outer(); Thread t1=o.getSal ...

  8. 内部类访问外部类的变量必须是final吗,java静态方法中不能引用非静态变量,静态方法中不能创建内部类的实例

    内部类访问外部类的变量必须是final吗? 如下: package com.java.concurrent; class A { int i = 3; public void shout() { cl ...

  9. this引用逃逸问题

    //this引用逃逸 // 1.构造器还未完成前,将自身this引用向外抛,使其他线程访问这个引用,进而访问到其未初始化的变量,造成问题 // 2.内部类访问外部类未初始化的成员变量 //3.多态继承 ...

随机推荐

  1. Jmeter运行过程中如何让Fiddler同时可以抓获到服务器的应答报文

    在默认情况下,Jmeter运行过程中,Fiddler是抓不到对应的应答报文的. 但是,在某些时候,我们希望分析Jmeter执行失败的原因,想了解Jmeter获取到的应答报文是否有问题,就需要同服务器返 ...

  2. 2018.09.07 bzoj1096: [ZJOI2007]仓库建设(斜率优化dp)

    传送门 斜率优化dp经典题. 令f[i]表示i这个地方修建仓库的最优值,那么答案就是f[n]. 用dis[i]表示i到1的距离,sump[i]表示1~i所有工厂的p之和,sum[i]表示1~i所有工厂 ...

  3. hdu-1175(bfs+剪枝)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1175 思路:用bfs,注意要转弯的次数,次数大于两次就跳过. #include<iostream ...

  4. 使用async-http-client实现异步批量http请求

    最近项目中需要在微服务中调用rest接口,而且需要调用得次数很多,所以同步得http客户端已经不满足要求,在网上查阅资料后发现了async-http-client这个包得性能不错,所以写了个demo测 ...

  5. Java 注解概要

    转载自:https://www.cnblogs.com/peida/archive/2013/04/24/3036689.html(Java注解就跟C#的特性是一样的) 要深入学习注解,我们就必须能定 ...

  6. Kolakoski

    Kolakoski序列:我们知道的还是太少 上帝创造了整数,其余的则是我们人类的事了.正因为如此,质数.完全数.Fibonacci 数之类的数列才会让数学家们如痴如醉,因为它们的存在是如此自然,没有任 ...

  7. jupyterlab notebook区别

    https://juejin.im/entry/5b350e52f265da59bd5ed31e Windows 7(Windows 10)安装后anaconda 命令行jupyter lab 出现4 ...

  8. AE和Mocha结合做视频后期制作

    AE:After Effects Mocha:视频图像追踪软件 智能抠像 前提:安装QuickTime视频编码器!4.1版,不然视频无法预览播放 >>关于AE CC自带的mocha 插件和 ...

  9. 一次简单完整的自动化登录测试-基于python+selenium进行cnblog的自动化登录测试

    Web登录测试是很常见的测试,手动测试大家再熟悉不过了,那如何进行自动化登录测试呢!本文就基于python+selenium结合unittest单元测试框架来进行一次简单但比较完整的cnblog自动化 ...

  10. hdu 3664 Permutation Counting(水DP)

    Permutation Counting Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Oth ...