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


免責聲明!

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



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