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

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

thread1.png

我們可以看到從Java1.2版本開始這個(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都需要訪問。線程1首先訪問了這塊內(nèi)存,并且添加了鎖。線程2這時(shí)候也想訪問這塊內(nèi)存,但由于線程1持有著鎖,所以線程2只能阻塞等待。但就在這個(gè)時(shí)候我們調(diào)用了線程1的stop()方法,會(huì)發(fā)生什么?

線程1立刻釋放了內(nèi)存鎖,線程2立刻獲取了內(nèi)存鎖。如果線程1原來(lái)在寫數(shù)據(jù)只寫了一半,也沒有機(jī)會(huì)寫了,也根本沒時(shí)間進(jìn)行清理了。這時(shí)候線程2拿到CPU的時(shí)間片開始讀內(nèi)存狀態(tài),結(jié)果發(fā)現(xiàn)內(nèi)存狀態(tài)是異常的,讀到了莫名其妙的數(shù)。因?yàn)榫€程1剛才還沒有來(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)單,我就不過多解釋了,唯一需要注意的是需要給boolean標(biāo)志位加上volatile關(guān)鍵字,因?yàn)?strong>isCancelled存在線程間可見性的問題。

  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~~~”這行文字;
并沒有輸出“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è)疑問就是為什么線程的靜態(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ì)其他線程立即可見
    OrderAccess::fence();
    //_SleepEvent相當(dāng)于Thread.sleep,表示如果線程調(diào)用了sleep方法,則通過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)于也是通過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、并且通過ParkEventunpark方法來(lái)喚醒線程。

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

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

thread3.png

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容