this引用逃逸(使用内部类获取未外部类未初始化完的变量),多态父类使用子类未初始化完的变量
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+构造器的组合语句,才表示创建对象。