本節講解的內容是線程的初始化、線程啟動、線程中斷、suspend()、resume()、stop()和優雅的關閉線程。
1.線程初始化和線程的啟動
首先看下Thread的構造方法:
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
當然Thread有多個構造方法,都是調用init方法初始化一個線程。看下init的定義:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
}
包含了所屬線程組、Runable對象、線程名稱、線程的棧大小、訪問控制權和inheritThreadLocals。當我們通過new關鍵字創建線程的時候,默認由當前線程來進行空間分配的,而創建出來的線程繼承了當前線程的是否為守護線程、線程優先級、contextClassLoader以及ThreadLocal。創建好的線程對象初始化完成後,在堆內存中等待執行。
注意,創建線程時最好執行線程的名字,方便通過命令jstack分析堆棧信息和問題排查。如果不手動指定線程名稱,默認是Thread-” + nextThreadNum()。
線程啟動是調用start()方法,當前線程同步告訴Java虛擬機,如果線程規劃器空閑,應該立即啟動線程。start方法不可以重複調用。
面試中經常會問到直接運行run()方法會怎麼?
首先run方法只是個普通方法,直接運行不會啟動一個線程,且當前只有一個主線程(當前線程),程序順序執行,達不到多線程的目的。且run方法可以重複執行。
2.線程的中斷
當我們執行一個阻塞IO或者長時間沒有響應的任務時,我們需要結束線程以避免長時間等待。Thread.interrupt(),不是立馬中斷線程,而是設置一個中斷標識位。Java中的阻塞函數比如Thread.sleep,Object.wait,Thread.join等,會不斷地輪訓檢測線程中斷標識位是否為true,如果為true,會立馬拋出InterruptedException,同時把標識位設置為false。在下面的程序中,有兩個線程,worker和sleeper,線程中斷後,觀察下輸出的標識位:
public class TestInterrupt {
public static void main(String[] args) {
Thread worker = new Thread(()->{
while (true){}
}, "work-thread");
Thread sleeper = new Thread(()->{
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "sleeper-thread");
worker.start();
sleeper.start();
worker.interrupt();
sleeper.interrupt();
System.out.println("work-thread interrupt is " + worker.isInterrupted());
System.out.println("sleeper-thread interrupt is " + sleeper.isInterrupted());
}
}
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at net.zhifou.concurrent.base.TestInterrupt.lambda$main$1(TestInterrupt.java:16)
at java.lang.Thread.run(Thread.java:748)
work-thread interrupt is true
sleeper-thread interrupt is false
當中斷阻塞狀態的sleeper線程,立馬拋出了InterruptedException異常,重置中斷標識位後輸出中斷標識位為false,worker線程中斷標識位仍為true。
Thread的兩個方法,interrupted和isInterrupted,都是返回中斷標識位。我們先看下方法定義:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
然後再看下isInterrupted方法的實現:
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
這個是native方法,看不到實現沒關係,看下注釋,根據ClearInterrupted的值重置或不重置。isInterrupted傳入的是false,不清除中斷的標識位。而interrupted,傳入的是ture,則返回中斷標識位後,然後清除中斷標識位。
3. suspend()、resume()、stop()
suspend()、resume()、stop()這三個方法,分別對應着,線程暫停、線程恢復、線程停止。觀察Java源碼,都被標識為@Deprecated,不建議使用了。
首先我們看suspend()、resume(),它們必須成對出現,且有先後順序。那麼為什麼會被廢棄呢?
因為線程調用suspend程序暫停後,不會釋放任何資源,包括鎖,且一直處於睡眠狀態,容易引發死鎖。而resume因為要和suspend成對出現,所以也沒有存在的必要。
那麼stop方法為什麼不建議使用呢?我們先看源碼:
@Deprecated
public final void stop() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
checkAccess();
if (this != Thread.currentThread()) {
security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
}
}
// A zero status value corresponds to "NEW", it can't change to
// not-NEW because we hold the lock.
if (threadStatus != 0) {
resume(); // Wake up thread if it was suspended; no-op otherwise
}
// The VM can handle all thread states
stop0(new ThreadDeath());
}
最終通過調用stop0(new ThreadDeath())函數,stop0是Native函數,參數是ThreadDeath,ThreadDeath是一個異常對象,該對象從Native層拋到了Java層。線程運行中遇到異常,會導致線程停止,不過不會引起程序退出。
線程是因為異常而停止的,鎖會釋放,但是不會釋放其他的資源,比如打開的socket連接或者IO,多不會關閉,引發內存泄漏。
很多時候為了保證數據安全,線程中會編寫同步代碼,如果當線程正在執行同步代碼時,此時調用stop,引起拋出異常,導致線程持有的鎖會全部釋放,此時就不能確保數據的安全性,出現無法預期的錯亂數據,還有可能導致存在需要被釋放的資源得不到釋放,引發內存泄露。所以用stop停止線程是不推薦的。
4.優雅的終止線程
關閉線程,肯定等到線程釋放掉資源,以免造成內存泄漏,所以不建議使用stop方法。那麼應該怎麼關閉線程呢?答案是兩階段終止模式。
兩階段終止模式,顧名思義,分兩個階段。第一個階段,向線程發送終止指令,記住這裡只是設置一個終止標識位。第二階段,線程響應指令,然後終止。這裡就用到了,Thread.interrupt()方法來設置終止標識位。我們先看一張圖如下:

想終止線程,也就是線程是終止狀態,如上圖所示,線程只能是RUNNABLE狀態才可以到終止狀態。所以線程目前狀態可以能是RUNNABLE狀態,或者是休眠狀態。當線程是休眠狀態,當我們設置中斷標識位時,線程立馬會拋出InterruptedException,並且JVM會清除中斷標識位,所以在捕獲InterruptedException異常後,需要重新設置中斷標識位。如下代碼:
public class StopThread {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("worker-Thread start ...");
while(!Thread.currentThread().isInterrupted()) {
System.out.println("worker-Thread working ...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
System.out.println("worker-Thread done ...");
}, "worker-Thread");
t.start();
Thread.sleep(10000);
t.interrupt();
}
}
線程t每次執行while內代碼,休眠1000ms,然後繼續執行,主線程在休眠10000ms後,線程t設置中斷標識位,此時,如果線程t還在sleep中,則拋出InterruptedException異常,JVM清除中斷標識位,重新設置中斷標識位後,下次執行while代碼,條件不符合,線程執行完畢。這樣,就完成了兩階段模式。
總結:
- 線程啟動使用start方法,而線程run方法,不會啟動一個線程。
- Thread.interrupt(),設置中斷標識位,具體是否結束線程,線程自己控制。
- suspend()、resume()、stop(),這三個方法已經不建議使用,有可能造成死鎖和內存泄漏。
- 通過兩階段提交模式,來優雅的終止線程。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/228015.html
微信掃一掃
支付寶掃一掃