1 來源
- 來源:《Java高併發編程詳解 多線程與架構設計》,汪文君著
- 章節:第一、二、三章
本文是前三章的筆記整理。
2 概述
本文主要講述了線程的生命週期、Thread
類的構造方法以及常用API
,最後介紹了線程的關閉方法。
3 線程生命週期
3.1 五個階段
線程生命週期可以分爲五個階段:
NEW
RUNNABLE
RUNNING
BLOCKED
TERMINATED
3.2 NEW
用new
創建一個Thread
對象時,但是並沒有使用start()
啓動線程,此時線程處於NEW
狀態。準確地說,只是Thread
對象的狀態,這就是一個普通的Java
對象。此時可以通過start()
方法進入RUNNABLE
狀態。
3.3 RUNNABLE
進入RUNNABLE
狀態必須調用start()
方法,這樣就在JVM
中創建了一個線程。但是,線程一經創建,並不能馬上被執行,線程執行與否需要聽令於CPU
調度,也就是說,此時是處於可執行狀態,具備執行的資格,但是並沒有真正執行起來,而是在等待被調度。
RUNNABLE
狀態只能意外終止或進入RUNNING
狀態。
3.4 RUNNING
一旦CPU
通過輪詢或其他方式從任務可執行隊列中選中了線程,此時線程才能被執行,也就是處於RUNNING
狀態,在該狀態中,可能發生的狀態轉換如下:
- 進入
TERMINATED
:比如調用已經不推薦的stop()
方法 - 進入
BLOCKED
:比如調用了sleep()
/wait()
方法,或者進行某個阻塞操作(獲取鎖資源、磁盤IO
等) - 進入
RUNNABLE
:CPU
時間片到,或者線程主動調用yield()
3.5 BLOCKED
也就是阻塞狀態,進入阻塞狀態的原因很多,常見的如下:
- 磁盤
IO
- 網絡操作
- 爲了獲取鎖而進入阻塞操作
處於BLOCKED
狀態時,可能發生的狀態轉換如下:
- 進入
TERMINATED
:比如調用不推薦的stop()
,或者JVM
意外死亡 - 進入
RUNNABLE
:比如休眠結束、被notify()
/nofityAll()
喚醒、獲取到某個鎖、阻塞過程被interrupt()
打斷等
3.6 TERMINATED
TERMINATED
是線程的最終狀態,進入該狀態後,意味着線程的生命週期結束,比如在下列情況下會進入該狀態:
- 線程運行正常結束
- 線程運行出錯意外結束
JVM
意外崩潰,導致所有線程都強制結束
4 Thread
構造方法
4.1 構造方法
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
狀態的線程才能修改。
4.2 線程的父子關係
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
。
4.3 關於stackSize
Thread
構造方法中有一個stackSize
參數,該參數指定了JVM
分配線程棧的地址空間的字節數,對平臺依賴性較高,在一些平臺上:
- 設置較大的值:可以使得線程內調用遞歸深度增加,降低
StackOverflowError
出現的概率 - 設置較低的值:可以使得創建的線程數增多,可以推遲
OutOfMemoryError
出現的時間
但是,在一些平臺上該參數不會起任何作用。另外,如果設置爲0也不會起到任何作用。
5 Thread API
5.1 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);
5.2 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
則不會
5.3 setPriority()
5.3.1 優先級介紹
線程與進程類似,也有自己的優先級,理論上來說,優先級越高的線程會有優先被調度的機會,但實際上並不是如此,設置優先級與yield()
類似,也是一個提醒性質的操作:
- 對於
root
用戶,會提醒操作系統想要設置的優先級別,否則會被忽略 - 如果
CPU
比較忙,設置優先級可能會獲得更多的CPU
時間片,但是空閒時優先級的高低幾乎不會有任何作用
所以,設置優先級只是很大程度上讓某個線程儘可能獲得比較多的執行機會,也就是讓線程自己儘可能被操作系統調度,而不是設置了高優先級就一定優先運行,或者說優先級高的線程比優先級低的線程就一定優先運行。
5.3.2 優先級源碼分析
設置優先級直接調用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。
5.4 interrupt()
interrupt()
是一個重要的API
,線程中斷的API
有如下三個:
void interrupt()
boolean isInterrupted()
static boolean interrupted()
下面對其逐一進行分析。
5.4.1 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();
}
會輸出線程被中斷的信息。
5.4.2 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
。
5.4.3 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()
就是這樣做的
5.5 join()
5.5.1 join()
簡介
join()
與sleep()
一樣,都是屬於可以中斷的方法,如果其他線程執行了對當前線程的interrupt
操作,也會捕獲到中斷信號,並且擦除線程的interrupt
標識,join()
提供了三個API
,分別如下:
void join()
void join(long millis,int nanos)
void join(long mills)
5.5.2 例子
一個簡單的例子如下:
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執行完畢後再執行。
6 線程關閉
Thread
中有一個過時的方法stop
,可以用於關閉線程,但是存在的問題是有可能不會釋放monitor
的鎖,因此不建議使用該方法關閉線程。線程的關閉可以分爲三類:
- 正常關閉
- 異常退出
- 假死
6.1 正常關閉
6.1.1 正常結束
線程運行結束後,就會正常退出,這是最普通的一種情況。
6.1.2 捕獲信號關閉線程
通過捕獲中斷信號去關閉線程,例子如下:
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();
}
6.1.3 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();
}
}
6.2 異常退出
線程執行單元中是不允許拋出checked
異常的,如果在線程運行過程中需要捕獲checked
異常並且判斷是否還有運行下去的必要,可以將checked
異常封裝爲unchecked
異常,比如RuntimeException
,拋出從而結束線程的生命週期。
6.3 假死
所謂假死就是雖然線程存在,但是卻沒有任何的外在表現,比如:
- 沒有日誌輸出
- 不進行任何的作業
等等,雖然此時線程是存在的,但看起來跟死了一樣,事實上是沒有死的,出現這種情況,很大可能是因爲線程出現了阻塞,或者兩個線程爭奪資源出現了死鎖。
這種情況需要藉助一些外部工具去判斷,比如VisualVM
、jconsole
等等,找出存在問題的線程以及當前的狀態,並判斷是哪個方法造成了阻塞。