本文是前三章的筆記整理。java
本文主要講述了線程的生命週期、Thread
類的構造方法以及經常使用API
,最後介紹了線程的關閉方法。編程
線程生命週期能夠分爲五個階段:bash
NEW
RUNNABLE
RUNNING
BLOCKED
TERMINATED
NEW
用new
建立一個Thread
對象時,可是並無使用start()
啓動線程,此時線程處於NEW
狀態。準確地說,只是Thread
對象的狀態,這就是一個普通的Java
對象。此時能夠經過start()
方法進入RUNNABLE
狀態。網絡
RUNNABLE
進入RUNNABLE
狀態必須調用start()
方法,這樣就在JVM
中建立了一個線程。可是,線程一經建立,並不能立刻被執行,線程執行與否須要聽令於CPU
調度,也就是說,此時是處於可執行狀態,具有執行的資格,可是並無真正執行起來,而是在等待被調度。多線程
RUNNABLE
狀態只能意外終止或進入RUNNING
狀態。架構
RUNNING
一旦CPU
經過輪詢或其餘方式從任務可執行隊列中選中了線程,此時線程才能被執行,也就是處於RUNNING
狀態,在該狀態中,可能發生的狀態轉換以下:併發
TERMINATED
:好比調用已經不推薦的stop()
方法BLOCKED
:好比調用了sleep()
/wait()
方法,或者進行某個阻塞操做(獲取鎖資源、磁盤IO
等)RUNNABLE
:CPU
時間片到,或者線程主動調用yield()
BLOCKED
也就是阻塞狀態,進入阻塞狀態的緣由不少,常見的以下:ide
IO
處於BLOCKED
狀態時,可能發生的狀態轉換以下:高併發
TERMINATED
:好比調用不推薦的stop()
,或者JVM
意外死亡RUNNABLE
:好比休眠結束、被notify()
/nofityAll()
喚醒、獲取到某個鎖、阻塞過程被interrupt()
打斷等TERMINATED
TERMINATED
是線程的最終狀態,進入該狀態後,意味着線程的生命週期結束,好比在下列狀況下會進入該狀態:工具
JVM
意外崩潰,致使全部線程都強制結束Thread
構造方法Thread
的構造方法一共有八個,這裏根據命名方式分類,使用默認命名的構造方法以下:
Thread()
Thread(Runnable target)
Thread(ThreadGroup group,Runnable target)
命名線程的構造方法以下:
Thread(String name)
Thread(Runnable target,Strintg name)
Thread(ThreadGroup group,String name)
Thread(ThreadGroup group,Runnable target,String name)
Thread(ThreadGroup group,Runnable target,String name,long stackSize)
但實際上全部的構造方法最終都是調用以下私有構造方法:
private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);
在默認命名構造方法中,在源碼中能夠看到,默認命名其實就是Thread-X
的命令(X爲數字):
public Thread() { this((ThreadGroup)null, (Runnable)null, "Thread-" + nextThreadNum(), 0L); } public Thread(Runnable target) { this((ThreadGroup)null, target, "Thread-" + nextThreadNum(), 0L); } private static synchronized int nextThreadNum() { return threadInitNumber++; }
而在命名構造方法就是自定義的名字。
另外,若是想修改線程的名字,能夠調用setName()
方法,可是須要注意,處於NEW
狀態的線程才能修改。
Thread
的全部構造方法都會調用以下方法:
private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);
其中的一段源碼截取以下:
if (name == null) { throw new NullPointerException("name cannot be null"); } else { this.name = name; Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { if (security != null) { g = security.getThreadGroup(); } if (g == null) { g = parent.getThreadGroup(); } } }
能夠看到當前這裏有一個局部變量叫parent
,而且賦值爲currentThread()
,currentThread()
是一個native
方法。由於一個線程被建立時的最初狀態爲NEW
,所以currentThread()
表明是建立自身線程的那個線程,也就是說,結論以下:
也就是本身建立的線程,父線程爲main
線程,而main
線程由JVM
建立。
另外,Thread
的構造方法中有幾個具備ThreadGroup
參數,該參數指定了線程位於哪個ThreadGroup
,若是一個線程建立的時候沒有指定ThreadGroup
,那麼將會和父線程同一個ThreadGroup
。main
線程所在的ThreadGroup
稱爲main
。
stackSize
Thread
構造方法中有一個stackSize
參數,該參數指定了JVM
分配線程棧的地址空間的字節數,對平臺依賴性較高,在一些平臺上:
StackOverflowError
出現的機率OutOfMemoryError
出現的時間可是,在一些平臺上該參數不會起任何做用。另外,若是設置爲0也不會起到任何做用。
Thread API
sleep()
sleep()
有兩個重載方法:
sleep(long mills)
sleep(long mills,int nanos)
可是在JDK1.5
後,引入了TimeUnit
,其中對sleep()
方法提供了很好的封裝,建議使用TimeUnit.XXXX.sleep()
去代替Thread.sleep()
:
TimeUnit.SECONDS.sleep(1); TimeUnit.MINUTES.sleep(3);
yield()
yield()
屬於一種啓發式方法,提醒CPU
調度器當前線程會自願放棄資源,若是CPU
資源不緊張,會忽略這種提醒。調用yield()
方法會使當前線程從RUNNING
變爲RUNNABLE
狀態。
關於yield()
與sleep()
的區別,區別以下:
sleep()
會致使當前線程暫停指定的時間,沒有CPU
時間片的消耗yield()
只是對CPU
調度器的一個提示,若是CPU
調度器沒有忽略這個提示,會致使線程上下文的切換sleep()
會使線程短暫阻塞,在給定時間內釋放CPU
資源yield()
生效,yield()
會使得從RUNNING
狀態進入RUNNABLE
狀態sleep()
會幾乎百分百地完成給定時間的休眠,可是yield()
的提示不必定能擔保sleep()
而另外一個線程調用interrupt()
會捕獲到中斷信號,而yield
則不會setPriority()
線程與進程相似,也有本身的優先級,理論上來講,優先級越高的線程會有優先被調度的機會,但實際上並非如此,設置優先級與yield()
相似,也是一個提醒性質的操做:
root
用戶,會提醒操做系統想要設置的優先級別,不然會被忽略CPU
比較忙,設置優先級可能會得到更多的CPU
時間片,可是空閒時優先級的高低幾乎不會有任何做用因此,設置優先級只是很大程度上讓某個線程儘量得到比較多的執行機會,也就是讓線程本身儘量被操做系統調度,而不是設置了高優先級就必定優先運行,或者說優先級高的線程比優先級低的線程就必定優先運行。
設置優先級直接調用setPriority()
便可,OpenJDK 11
源碼以下:
public final void setPriority(int newPriority) { this.checkAccess(); if (newPriority <= 10 && newPriority >= 1) { ThreadGroup g; if ((g = this.getThreadGroup()) != null) { if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority(); } this.setPriority0(this.priority = newPriority); } } else { throw new IllegalArgumentException(); } }
能夠看到優先級處於[1,10]
之間,並且不能設置爲大於當前ThreadGroup
的優先級,最後經過native
方法setPriority0
設置優先級。
通常狀況下,不會對線程的優先級設置級別,默認狀況下,線程的優先級爲5,由於main
線程的優先級爲5,並且main
爲全部線程的父進程,所以默認狀況下線程的優先級也是5。
interrupt()
interrupt()
是一個重要的API
,線程中斷的API
有以下三個:
void interrupt()
boolean isInterrupted()
static boolean interrupted()
下面對其逐一進行分析。
interrupt()
一些方法調用會使得當前線程進入阻塞狀態,好比:
Object.wait()
Thread.sleep()
Thread.join()
Selector.wakeup()
而調用interrupt()
能夠打斷阻塞,打斷阻塞並不等於線程的生命週期結束,僅僅是打斷了當前線程的阻塞狀態。一旦在阻塞狀態下被打斷,就會拋出一個InterruptedException
的異常,這個異常就像一個信號同樣通知當前線程被打斷了,例子以下:
public static void main(String[] args) throws InterruptedException{ Thread thread = new Thread(()->{ try{ TimeUnit.SECONDS.sleep(10); }catch (InterruptedException e){ System.out.println("Thread is interrupted."); } }); thread.start(); TimeUnit.SECONDS.sleep(1); thread.interrupt(); }
會輸出線程被中斷的信息。
isInterrupted()
isInterrupted()
能夠判斷當前線程是否被中斷,僅僅是對interrupt()
標識的一個判斷,並不會影響標識發生任何改變(由於調用interrupt()
的時候會設置內部的一個叫interrupt flag
的標識),例子以下:
public static void main(String[] args) throws InterruptedException{ Thread thread = new Thread(()->{ while (true){} }); thread.start(); TimeUnit.SECONDS.sleep(1); System.out.println("Thread is interrupted :"+thread.isInterrupted()); thread.interrupt(); System.out.println("Thread is interrupted :"+thread.isInterrupted()); }
輸出結果爲:
Thread is interrupted :false Thread is interrupted :true
另外一個例子以下:
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { System.out.println("In catch block thread is interrupted :" + isInterrupted()); } } } }; thread.start(); TimeUnit.SECONDS.sleep(1); System.out.println("Thread is interrupted :" + thread.isInterrupted()); thread.interrupt(); TimeUnit.SECONDS.sleep(1); System.out.println("Thread is interrupted :" + thread.isInterrupted()); }
輸出結果:
Thread is interrupted :false In catch block thread is interrupted :false Thread is interrupted :false
一開始線程未被中斷,結果爲false
,調用中斷方法後,在循環體內捕獲到了異常(信號),此時會Thread
自身會擦除interrupt
標識,將標識復位,所以捕獲到異常後輸出結果也爲false
。
interrupted()
這是一個靜態方法,調用該方法會擦除掉線程的interrupt
標識,須要注意的是若是當前線程被打斷了:
interrupted()
會返回true
,而且當即擦除掉interrupt
標識false
,除非在此期間線程又一次被打斷例子以下:
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread() { @Override public void run() { while (true) { System.out.println(Thread.interrupted()); } } }; thread.setDaemon(true); thread.start(); TimeUnit.MILLISECONDS.sleep(2); thread.interrupt(); }
輸出(截取一部分):
false false false true false false false
能夠看到其中帶有一個true
,也就是interrupted()
判斷到了其被中斷,此時會當即擦除中斷標識,而且只有該次返回true
,後面都是false
。
關於interrupted()
與isInterrupted()
的區別,能夠從源碼(OpenJDK 11
)知道:
public static boolean interrupted() { return currentThread().isInterrupted(true); } public boolean isInterrupted() { return this.isInterrupted(false); } @HotSpotIntrinsicCandidate private native boolean isInterrupted(boolean var1);
實際上二者都是調用同一個native
方法,其中的布爾變量表示是否擦除線程的interrupt
標識:
true
表示想要擦除,interrupted()
就是這樣作的false
表示不想擦除,isInterrupted()
就是這樣作的join()
join()
簡介join()
與sleep()
同樣,都是屬於能夠中斷的方法,若是其餘線程執行了對當前線程的interrupt
操做,也會捕獲到中斷信號,而且擦除線程的interrupt
標識,join()
提供了三個API
,分別以下:
void join()
void join(long millis,int nanos)
void join(long mills)
一個簡單的例子以下:
public class Main { public static void main(String[] args) throws InterruptedException { List<Thread> threads = IntStream.range(1,3).mapToObj(Main::create).collect(Collectors.toList()); threads.forEach(Thread::start); for (Thread thread:threads){ thread.join(); } for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+" # "+i); shortSleep(); } } private static Thread create(int seq){ return new Thread(()->{ for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+" # "+i); shortSleep(); } },String.valueOf(seq)); } private static void shortSleep(){ try{ TimeUnit.MILLISECONDS.sleep(2); }catch (InterruptedException e){ e.printStackTrace(); } } }
輸出截取以下:
2 # 8 1 # 8 2 # 9 1 # 9 main # 0 main # 1 main # 2 main # 3 main # 4
線程1和線程2交替執行,而main
線程會等到線程1和線程2執行完畢後再執行。
Thread
中有一個過期的方法stop
,能夠用於關閉線程,可是存在的問題是有可能不會釋放monitor
的鎖,所以不建議使用該方法關閉線程。線程的關閉能夠分爲三類:
線程運行結束後,就會正常退出,這是最普通的一種狀況。
經過捕獲中斷信號去關閉線程,例子以下:
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { System.out.println("work..."); while(!isInterrupted()){ } System.out.println("exit..."); } }; t.start(); TimeUnit.SECONDS.sleep(5); System.out.println("System will be shutdown."); t.interrupt(); }
一直檢查interrupt
標識是否設置爲true
,設置爲true
則跳出循環。另外一種方式是使用sleep()
:
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { System.out.println("work..."); while(true){ try{ TimeUnit.MILLISECONDS.sleep(1); }catch (InterruptedException e){ break; } } System.out.println("exit..."); } }; t.start(); TimeUnit.SECONDS.sleep(5); System.out.println("System will be shutdown."); t.interrupt(); }
volatile
因爲interrupt
標識頗有可能被擦除,或者不會調用interrupt()
方法,所以另外一種方法是使用volatile
修飾一個布爾變量,並不斷循環判斷:
public class Main { static class MyTask extends Thread{ private volatile boolean closed = false; @Override public void run() { System.out.println("work..."); while (!closed && !isInterrupted()){ } System.out.println("exit..."); } public void close(){ this.closed = true; this.interrupt(); } } public static void main(String[] args) throws InterruptedException { MyTask t = new MyTask(); t.start(); TimeUnit.SECONDS.sleep(5); System.out.println("System will be shutdown."); t.close(); } }
線程執行單元中是不容許拋出checked
異常的,若是在線程運行過程當中須要捕獲checked
異常而且判斷是否還有運行下去的必要,能夠將checked
異常封裝爲unchecked
異常,好比RuntimeException
,拋出從而結束線程的生命週期。
所謂假死就是雖然線程存在,可是卻沒有任何的外在表現,好比:
等等,雖然此時線程是存在的,但看起來跟死了同樣,事實上是沒有死的,出現這種狀況,很大多是由於線程出現了阻塞,或者兩個線程爭奪資源出現了死鎖。
這種狀況須要藉助一些外部工具去判斷,好比VisualVM
、jconsole
等等,找出存在問題的線程以及當前的狀態,並判斷是哪一個方法形成了阻塞。