Java中停止線程的正確姿勢(shì)(從源碼角度理解)

眾所周知在Thread類的API中有一個(gè)停止線程的方法stop()脸侥,但是它是不安全的晓殊,我們可以看一下Oracle的官方API中是怎樣解釋這個(gè)方法的:

thread1.png

我們可以看到從Java1.2版本開(kāi)始這個(gè)方法就被廢棄了~

廢棄的原因是:如果調(diào)用stop()方法去停止一個(gè)線程唱凯,會(huì)去釋放該線程所持有的全部鎖尔许,這樣就會(huì)導(dǎo)致被釋放鎖保護(hù)的對(duì)象們進(jìn)入一個(gè)不一致的狀態(tài),這種狀態(tài)也可以被稱為損壞狀態(tài)诀浪。當(dāng)線程對(duì)被損壞狀態(tài)的對(duì)象進(jìn)行操作時(shí)棋返,可能會(huì)產(chǎn)生意想不到的嚴(yán)重后果,并且難以發(fā)現(xiàn)雷猪。

舉個(gè)栗子:
有一塊共享內(nèi)存區(qū)域睛竣,線程1和線程2都需要訪問(wèn)。線程1首先訪問(wèn)了這塊內(nèi)存求摇,并且添加了鎖射沟。線程2這時(shí)候也想訪問(wèn)這塊內(nèi)存,但由于線程1持有著鎖与境,所以線程2只能阻塞等待验夯。但就在這個(gè)時(shí)候我們調(diào)用了線程1的stop()方法,會(huì)發(fā)生什么嚷辅?

線程1立刻釋放了內(nèi)存鎖簿姨,線程2立刻獲取了內(nèi)存鎖距误。如果線程1原來(lái)在寫數(shù)據(jù)只寫了一半簸搞,也沒(méi)有機(jī)會(huì)寫了扁位,也根本沒(méi)時(shí)間進(jìn)行清理了。這時(shí)候線程2拿到CPU的時(shí)間片開(kāi)始讀內(nèi)存狀態(tài)趁俊,結(jié)果發(fā)現(xiàn)內(nèi)存狀態(tài)是異常的域仇,讀到了莫名其妙的數(shù)。因?yàn)榫€程1剛才還沒(méi)有來(lái)得及清理就掛了寺擂,留下了爛攤子給線程2暇务,這時(shí)候如果線程2處理不來(lái)這個(gè)爛攤子,就可能會(huì)Crash了怔软。

這樣的操作是非常危險(xiǎn)的垦细,也正是因?yàn)檫@樣的原因,基本上不管是什么語(yǔ)言挡逼,在線程這塊都把它們直接停止線程的方法廢棄掉了括改。

上面巴拉巴拉說(shuō)了一堆,那么到底應(yīng)該怎樣去停止一個(gè)線程呢家坎?

線程這個(gè)東西呢嘱能,其實(shí)是任務(wù)執(zhí)行的一個(gè)設(shè)計(jì)。也即是說(shuō)線程和任務(wù)是一種強(qiáng)綁定的關(guān)系虱疏,任務(wù)執(zhí)行完了惹骂,線程也就結(jié)束了。所以線程的執(zhí)行模式就是一個(gè)協(xié)作的任務(wù)執(zhí)行模式做瞪。既然線程不能直接被停止对粪,那么我們可以讓任務(wù)結(jié)束,線程自然也就停止了装蓬。

也就是說(shuō)如果我們想要停止某個(gè)線程衩侥,一定需要有個(gè)前提:目標(biāo)線程應(yīng)當(dāng)具有處理中斷線程的能力。

具體做法:

  • boolean標(biāo)志位
  • Interrupt原生支持
  1. boolean標(biāo)志位退出法:
public class ThreadFlagTest {

    public static void main(String[] args) {
        FlagThread flagThread = new FlagThread();
        flagThread.start();
        flagThread.cancel();
    }

    public static class FlagThread extends Thread {
        private volatile boolean isCancelled;

        public void run() {
            while (!isCancelled) {
                //do something
            }
        }

        public void cancel() {
            isCancelled = true;
        }
    }
}

代碼非常簡(jiǎn)單矛物,我就不過(guò)多解釋了茫死,唯一需要注意的是需要給boolean標(biāo)志位加上volatile關(guān)鍵字,因?yàn)?strong>isCancelled存在線程間可見(jiàn)性的問(wèn)題履羞。

  1. Interrupt的原生支持:
  • void interrupt()
    如果線程處于被阻塞狀態(tài)(例如處于 sleep, wait, join 等狀態(tài))峦萎,那么線程將立即退出被阻塞狀態(tài),并拋出一個(gè) InterruptedException 異常
    如果線程處于正骋涫祝活動(dòng)狀態(tài)爱榔,那么會(huì)將該線程的中斷標(biāo)志設(shè)置為 true。被設(shè)置中斷標(biāo)志的線程將繼續(xù)正常運(yùn)行糙及,不受影響详幽。

  • static boolean interrupted()
    測(cè)試當(dāng)前線程(正在執(zhí)行這一命令的線程)是否被中斷。這一調(diào)用會(huì)將當(dāng)前線程的中斷狀態(tài)重置為 false

  • boolean isInterrupted()
    測(cè)試線程是否被終止。不像靜態(tài)的中斷方法唇聘,這一調(diào)用不改變線程的中斷狀態(tài)

我們需要知道的是interrupt() 方法并不能真正的中斷線程版姑,需要被調(diào)用的線程自己進(jìn)行配合才行,可以在調(diào)用阻塞方法時(shí)正確處理 InterruptedException 異常(例如迟郎,catch 異常后就結(jié)束線程)

public class ThreadInterruptTest {

    public static void main(String[] args) {
        InterruptThread interruptThread = new InterruptThread();
        interruptThread.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        interruptThread.interrupt();//中斷通知
    }

    //目標(biāo)線程
    static class InterruptThread extends Thread {
        @Override
        public void run() {
            super.run();
            try {
                Thread.sleep(5000);
                System.out.println("Done~~~~");
            } catch (InterruptedException e) {
                //這里可以進(jìn)行線程中斷后的清理工作
                System.out.println("Interrupt~~~~");
                Thread.currentThread().interrupt();
                System.out.println(Thread.currentThread().isInterrupted());
            }
        }
    }

在上面的這個(gè)例子里面剥险,最終的執(zhí)行結(jié)果是:
輸出“Interrupt~~~”這行文字;
并沒(méi)有輸出“Done~~~”這行文字宪肖;
說(shuō)明當(dāng)我們?cè)谥骶€程中調(diào)用InterruptThread線程的interrupt()方法后表制,InterruptThread線程就被中斷了。
但是有一點(diǎn)需要注意控乾,我們來(lái)看一下Thread類的API文檔:

thread2.png

Thread.sleep 這個(gè)阻塞方法么介,接收到中斷請(qǐng)求,會(huì)拋出 InterruptedException蜕衡,讓上層代碼處理夭拌。這時(shí),可以什么都不做衷咽,結(jié)果就是中斷標(biāo)記會(huì)被重新設(shè)置為 false鸽扁!看 Thread.sleep方法的注釋,也強(qiáng)調(diào)了這點(diǎn)镶骗。
在接收到中斷請(qǐng)求時(shí)桶现,標(biāo)準(zhǔn)做法是執(zhí)行 Thread.currentThread().interrupt() 恢復(fù)中斷,讓線程退出鼎姊。
所以上面的例子里面我們的代碼執(zhí)行后打印的中斷標(biāo)記是true骡和。

講到這里肯定有同學(xué)會(huì)好奇,interrupt()方法底層到底是如何去實(shí)現(xiàn)的呢相寇?現(xiàn)在讓我們走進(jìn)interrupt()方法的native世界去看一下慰于。

我們首先去解決一個(gè)疑問(wèn)就是為什么線程的靜態(tài)方法interrupted()會(huì)把線程的中斷狀態(tài)重置為false,而isInterrupted()不會(huì)改變中斷狀態(tài)唤衫?

Thread.java類的相關(guān)源碼:

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
        return isInterrupted(false);
}

private native boolean isInterrupted(boolean ClearInterrupted);

native層源碼:

bool os::is_interrupted(Thread* thread, bool clear_interrupted) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");

  OSThread* osthread = thread->osthread();

  bool interrupted = osthread->interrupted();

  if (interrupted && clear_interrupted) {
    osthread->set_interrupted(false);
    // consider thread->_SleepEvent->reset() ... optional optimization
  }

  return interrupted;
}

看了源碼立刻恍然大悟婆赠,原來(lái)就是只有當(dāng)傳遞的參數(shù)ClearInterruptedtrue的時(shí)候才會(huì)重置中斷狀態(tài)為false,毫無(wú)神秘感可言佳励。

接下來(lái)我們重點(diǎn)分析下Thread類的interrupt()方法:
native層源碼:

void os::interrupt(Thread* thread) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");
  //獲取本地線程對(duì)象
  OSThread* osthread = thread->osthread();

  if (!osthread->interrupted()) {
    osthread->set_interrupted(true);//設(shè)置中斷狀態(tài)為true
    // More than one thread can get here with the same value of osthread,
    // resulting in multiple notifications.  We do, however, want the store
    // to interrupted() to be visible to other threads before we execute unpark().
    //使得interrupted狀態(tài)對(duì)其他線程立即可見(jiàn)
    OrderAccess::fence();
    //_SleepEvent相當(dāng)于Thread.sleep休里,表示如果線程調(diào)用了sleep方法,則通過(guò)unpark喚醒
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }

  // For JSR166. Unpark even if interrupt status already was set
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();
  //_ParkEvent用于synchronized同步塊和Object.wait()赃承,這里相當(dāng)于也是通過(guò)unpark進(jìn)行喚醒
  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;

}
JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
  JVMWrapper("JVM_Sleep");

  if (millis < 0) {
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  //判斷并清除線程中斷狀態(tài)妙黍,如果中斷狀態(tài)為true,拋出中斷異常
  if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
    THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
  }

  // Save current thread state and restore it at the end of this block.
  // And set new thread state to SLEEPING.
  JavaThreadSleepState jtss(thread);
...

上面源碼里面重要的代碼行邏輯我都加上中文注釋了,thread.interrupt()方法就是設(shè)置interrupted狀態(tài)為true瞧剖、并且通過(guò)ParkEventunpark方法來(lái)喚醒線程拭嫁。

同時(shí)通過(guò)源碼我們也知道了當(dāng)中斷狀態(tài)為true的時(shí)候可免,Object.waitThread.sleep做粤、Thread.join會(huì)拋出InterruptedException浇借,這里我們只看了sleep()的native層源碼。

最后我們通過(guò)一張圖來(lái)總結(jié)下吧:

thread3.png

結(jié)論就是如果能用boolean 標(biāo)志位的情況驮宴,盡量使用boolean標(biāo)志位逮刨,畢竟調(diào)用jni是有性能開(kāi)銷的呕缭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末堵泽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子恢总,更是在濱河造成了極大的恐慌迎罗,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件片仿,死亡現(xiàn)場(chǎng)離奇詭異纹安,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)砂豌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門厢岂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人阳距,你說(shuō)我怎么就攤上這事塔粒。” “怎么了筐摘?”我有些...
    開(kāi)封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵卒茬,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我咖熟,道長(zhǎng)圃酵,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任馍管,我火速辦了婚禮郭赐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘确沸。我一直安慰自己堪置,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布张惹。 她就那樣靜靜地躺著舀锨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宛逗。 梳的紋絲不亂的頭發(fā)上坎匿,一...
    開(kāi)封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼替蔬。 笑死告私,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的承桥。 我是一名探鬼主播驻粟,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼凶异!你這毒婦竟也來(lái)了蜀撑?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤剩彬,失蹤者是張志新(化名)和其女友劉穎酷麦,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體喉恋,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沃饶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了轻黑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片糊肤。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖氓鄙,靈堂內(nèi)的尸體忽然破棺而出馆揉,到底是詐尸還是另有隱情,我是刑警寧澤玖详,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布把介,位于F島的核電站,受9級(jí)特大地震影響蟋座,放射性物質(zhì)發(fā)生泄漏拗踢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一向臀、第九天 我趴在偏房一處隱蔽的房頂上張望巢墅。 院中可真熱鬧,春花似錦券膀、人聲如沸君纫。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蓄髓。三九已至,卻和暖如春舒帮,著一層夾襖步出監(jiān)牢的瞬間会喝,已是汗流浹背陡叠。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肢执,地道東北人枉阵。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像预茄,于是被迫代替她去往敵國(guó)和親兴溜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容