java this引用逃逸詳解
1、什么是This逃逸?
在構(gòu)造器構(gòu)造還未徹底完成前(即實(shí)例初始化階段還未完成),將自身this引用向外拋出并被其他線程復(fù)制(訪問)了該引用,可能會(huì)問到該還未被初始化的變量,甚至可能會(huì)造成更大嚴(yán)重的問題。
廢話不多說,看一下代碼
/**
* 模擬this逃逸
* @author Lijian
*
*/
public class ThisEscape {
//final常量會(huì)保證在構(gòu)造器內(nèi)完成初始化(但是僅限于未發(fā)生this逃逸的情況下,具體可以看多線程對(duì)final保證可見性的實(shí)現(xiàn))
final int i;
//盡管實(shí)例變量有初始值,但是還實(shí)例化完成
int j = 0;
static ThisEscape obj;
public ThisEscape() {
i=1;
j=1;
//將this逃逸拋出給線程B
obj = this;
}
public static void main(String[] args) {
//線程A:模擬構(gòu)造器中this逃逸,將未構(gòu)造完全對(duì)象引用拋出
/*Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//obj = new ThisEscape();
}
});*/
//線程B:讀取對(duì)象引用,訪問i/j變量
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
//可能會(huì)發(fā)生初始化失敗的情況解釋:實(shí)例變量i的初始化被重排序到構(gòu)造器外,此時(shí)1還未被初始化
ThisEscape objB = obj;
try {
System.out.println(objB.j);
} catch (NullPointerException e) {
System.out.println("發(fā)生空指針錯(cuò)誤:普通變量j未被初始化");
}
try {
System.out.println(objB.i);
} catch (NullPointerException e) {
System.out.println("發(fā)生空指針錯(cuò)誤:final變量i未被初始化");
}
}
});
//threadA.start();
threadB.start();
}
}
輸出結(jié)果:這說明ThisEscape還未完成實(shí)例化,構(gòu)造還未徹底結(jié)束。
發(fā)生空指針錯(cuò)誤:普通變量j未被初始化
發(fā)生空指針錯(cuò)誤:final變量i未被初始化
另一種情況是利用線程A模擬this逃逸,但不一定會(huì)發(fā)生,線程A模擬構(gòu)造器正在構(gòu)造...而線程B嘗試訪問變量,這是因?yàn)?/p>
(1)由于JVM的指令重排序存在,實(shí)例變量i的初始化被安排到構(gòu)造器外(final可見性保證是final變量規(guī)定在構(gòu)造器中完成的);
(2)類似于this逃逸,線程A中構(gòu)造器構(gòu)造還未完全完成。
所以嘗試多次輸出(相信我一定會(huì)發(fā)生的,只是概率相對(duì)低),也會(huì)發(fā)生類似this引用逃逸的情況。
/**
* 模擬this逃逸
* @author Lijian
*
*/
public class ThisEscape {
//final常量會(huì)保證在構(gòu)造器內(nèi)完成初始化(但是僅限于未發(fā)送this逃逸的情況下)
final int i;
//盡管實(shí)例變量有初始值,但是還實(shí)例化完成
int j = 0;
static ThisEscape obj;
public ThisEscape() {
i=1;
j=1;
//obj = this ;
}
public static void main(String[] args) {
//線程A:模擬構(gòu)造器中this逃逸,將未構(gòu)造完全對(duì)象引用拋出
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//構(gòu)造初始化中...線程B可能獲取到還未被初始化完成的變量
//類似于this逃逸,但并不定發(fā)生
obj = new ThisEscape();
}
});
//線程B:讀取對(duì)象引用,訪問i/j變量
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
//可能會(huì)發(fā)生初始化失敗的情況解釋:實(shí)例變量i的初始化被重排序到構(gòu)造器外,此時(shí)1還未被初始化
ThisEscape objB = obj;
try {
System.out.println(objB.j);
} catch (NullPointerException e) {
System.out.println("發(fā)生空指針錯(cuò)誤:普通變量j未被初始化");
}
try {
System.out.println(objB.i);
} catch (NullPointerException e) {
System.out.println("發(fā)生空指針錯(cuò)誤:final變量i未被初始化");
}
}
});
threadA.start();
threadB.start();
}
}
2、什么情況下會(huì)This逃逸?
(1)在構(gòu)造器中很明顯地拋出this引用提供其他線程使用(如上述的明顯將this拋出)。
(2)在構(gòu)造器中內(nèi)部類使用外部類情況:內(nèi)部類訪問外部類是沒有任何條件的,也不要任何代價(jià),也就造成了當(dāng)外部類還未初始化完成的時(shí)候,內(nèi)部類就嘗試獲取為初始化完成的變量
- 在構(gòu)造器中啟動(dòng)線程:?jiǎn)?dòng)的線程任務(wù)是內(nèi)部類,在內(nèi)部類中xxx.this訪問了外部類實(shí)例,就會(huì)發(fā)生訪問到還未初始化完成的變量
- 在構(gòu)造器中注冊(cè)事件,這是因?yàn)樵跇?gòu)造器中監(jiān)聽事件是有回調(diào)函數(shù)(可能訪問了操作了實(shí)例變量),而事件監(jiān)聽一般都是異步的。在還未初始化完成之前就可能發(fā)生回調(diào)訪問了未初始化的變量。
在構(gòu)造器中啟動(dòng)線程代碼實(shí)現(xiàn):
/**
* 模擬this逃逸2:構(gòu)造器中啟動(dòng)線程
* @author Lijian
*
*/
public class ThisEscape2 {
final int i;
int j;
public ThisEscape2() {
i = 1;
j = 1;
new Thread(new RunablTest()).start();
}
//內(nèi)部類實(shí)現(xiàn)Runnable:引用外部類
private class RunablTest implements Runnable{
@Override
public void run() {
try {
System.out.println(ThisEscape2.this.j);
} catch (NullPointerException e) {
System.out.println("發(fā)生空指針錯(cuò)誤:普通變量j未被初始化");
}
try {
System.out.println(ThisEscape2.this.i);
} catch (NullPointerException e) {
System.out.println("發(fā)生空指針錯(cuò)誤:final變量i未被初始化");
}
}
}
public static void main(String[] args) {
new ThisEscape2();
}
}
構(gòu)造器中注冊(cè)事件,引用網(wǎng)上的一段偽代碼將以解釋:
public class ThisEscape3 {
private final int var;
public ThisEscape3(EventSource source) { //注冊(cè)事件,會(huì)一直監(jiān)聽,當(dāng)發(fā)生事件e時(shí),會(huì)執(zhí)行回調(diào)函數(shù)doSomething
source.registerListener( //匿名內(nèi)部類實(shí)現(xiàn)
new EventListener() {
public void onEvent(Event e) { //此時(shí)ThisEscape3可能還未初始化完成,var可能還未被賦值,自然就發(fā)生嚴(yán)重錯(cuò)誤
doSomething(e);
}
}
);
var = 10;
}
// 在回調(diào)函數(shù)中訪問變量
int doSomething(Event e) {
return var;
}
}
3、怎樣避免This逃逸?
?。?)單獨(dú)編寫一個(gè)啟動(dòng)線程的方法,不要在構(gòu)造器中啟動(dòng)線程,嘗試在外部啟動(dòng)。
...
private Thread t;
public ThisEscape2() {
t = new Thread(new EscapeRunnable());
}
public void initStart() {
t.start();
}
...
(2)將事件監(jiān)聽放置于構(gòu)造器外,比如new Object()的時(shí)候就啟動(dòng)事件監(jiān)聽,但是在構(gòu)造器內(nèi)不能使用事件監(jiān)聽,那可以在static{}中加事件監(jiān)聽,這樣就跟構(gòu)造器解耦了
static{
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
}
);
var = 10;
}
}
4、總結(jié)
this引用逃逸問題實(shí)則是Java多線程編程中需要注意的問題,引起逃逸的原因無(wú)非就是在多線程的編程中“濫用”引用(往往涉及構(gòu)造器中顯式或隱式地濫用this引用),在使用到this引用的時(shí)候需要特別注意!
同時(shí)這會(huì)涉及到:final的內(nèi)存語(yǔ)義,即final域禁止重排序問題(2020.11.22增加),包括寫final域與讀final域重排序兩個(gè)規(guī)則(參考資料《Java并發(fā)編程的藝術(shù)》)
以上就是java this引用逃逸詳解的詳細(xì)內(nèi)容,更多關(guān)于java this引用逃逸的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
多模字符串匹配算法原理及Java實(shí)現(xiàn)代碼
這篇文章主要介紹了多模字符串匹配算法原理及Java實(shí)現(xiàn)代碼,涉及算法背景,原理,構(gòu)建過程簡(jiǎn)單介紹幾Java代碼實(shí)現(xiàn)等相關(guān)內(nèi)容,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
SpringBoot中的FailureAnalyzer使用詳解
這篇文章主要介紹了SpringBoot中的FailureAnalyzer使用詳解,Spring Boot的FailureAnalyzer是一個(gè)接口,它用于在Spring Boot應(yīng)用啟動(dòng)失敗時(shí)提供有關(guān)錯(cuò)誤的詳細(xì)信息,這對(duì)于開發(fā)者來(lái)說非常有用,因?yàn)樗梢詭椭覀兛焖僮R(shí)別問題并找到解決方案,需要的朋友可以參考下2023-12-12
Spring Boot 將yyyy-MM-dd格式的文本字符串直接轉(zhuǎn)換為L(zhǎng)ocalDateTime出現(xiàn)的問題
這篇文章主要介紹了Spring Boot 將yyyy-MM-dd格式的文本字符串直接轉(zhuǎn)換為L(zhǎng)ocalDateTime出現(xiàn)的問題,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
Springboot集成第三方j(luò)ar快速實(shí)現(xiàn)微信、支付寶等支付場(chǎng)景
這篇文章主要介紹了Springboot集成第三方j(luò)ar快速實(shí)現(xiàn)微信、支付寶等支付場(chǎng)景,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
idea 有時(shí)提示找不到類或者符號(hào)的解決
這篇文章主要介紹了idea 有時(shí)提示找不到類或者符號(hào)的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2020-09-09

