1、什麼是This逃逸? 在構造器構造還未徹底完成前(即實例初始化階段還未完成),將自身this引用向外拋出並被其他線程複製(訪問)了該引用,可能會問到該還未被初始化的變數,甚至可能會造成更大嚴重的問題。 廢話不多說,看一下代碼 輸出結果:這說明ThisEscape還未完成實例化,構造還未徹底結束。 ...
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引用的時候需要特別註意!