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+構造器的組合語句,才表示創建對象

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM