前言
Thread
類想必都不陌生,第一次學習多線程的時候就一定會接觸Thread
類。本篇主要從Thread
類的定義、使用、注意事項、源碼等方面入手,全方位的講解Thread
類。
Thread
我們經常會被問到這樣一個問題:
Java開啓一個新線程有哪幾種方法?
答案是兩種:繼承Thread
類、實現Runnable
接口。
說只有兩種,有人可能就不服了,實現Callable
接口爲什麼不算?線程池爲什麼不算?
Oracle官方說明如下:
https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
其中已經寫得很明白
There are two ways to create a new thread of execution.
One is to declare a class to be a subclass of Thread.This subclass should override the run method of class Thread.
The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method.
有兩種方式創建一個新的執行線程
一種是定義Thread的子類,子類重寫run方法
另一種是定義Runnable接口的實現類,實現run方法
至於爲什麼實現Callable
接口和線程池不算,以後的博客會詳細介紹。
一個小問題相信已經讓大家回憶起了Thread
類相關的知識,接下來就從源碼的角度解析Thread
類
定義
Thread
類從JDK1.0版本開始就有了,可謂是歷史悠久。本篇以JDK1.8爲例進行源碼講解,Thread
類定義如下(只列出需要重點關注的成員變量、常量):
// Thread類實現了Runnable接口
public class Thread implements Runnable {
// 是否是守護線程
private boolean daemon = false;
// 最小優先級
public final static int MIN_PRIORITY = 1;
// 默認優先級
public final static int NORM_PRIORITY = 5;
// 最大優先級
public final static int MAX_PRIORITY = 10;
// 線程名稱
private volatile char name[];
// 線程優先級
private int priority;
// 需要執行的單元
private Runnable target;
// 線程狀態
private volatile int threadStatus = 0;
// 線程ID
private long tid;
}
從Thread
類的定義可以看出,對於一個Thread,需要重點關注的有以下幾點:
- 實現了Runnable接口
- 線程需要重點關注的四個屬性:ID、Name、是否是守護線程、優先級
- 線程的狀態需要特別注意
接下來就從這三點分別進行詳細講解,因爲線程的狀態之前已經專門寫過一篇博客:Java線程到底有幾種狀態。所以重點講解其餘的兩點
實現Runnable接口
前文說到Java開啓一個新線程的兩種方式:繼承Thread
類,重寫run
方法;實現Runnable
接口,實現run
方法。接下來就來看一下Thread
類中的run
方法:
@Override
public void run() {
if (target != null) {
target.run();
}
}
其中target
是Runnable
類型的引用,也可以看做線程的執行單元,結合下面一個小實例:
/**
* @author sicimike
*/
public class CreateThreadDemo {
public static void main(String[] args) {
new SicThread1().start();
new Thread(new SicThread2()).start();
}
}
class SicThread1 extends Thread {
@Override
public void run() {
System.out.println("extends Thread");
}
}
class SicThread2 implements Runnable {
@Override
public void run() {
System.out.println("implements Runnable");
}
}
在代碼
new SicThread1().start();
中調用的SicThread1
的start
方法,間接調用重寫的run
方法。
SicThread2
直接實現了Runnable
接口,在代碼
new Thread(new SicThread2()).start();
中調用的是構造方法public Thread(Runnable target) {...}
。
由於Thread類實現了Runnable
接口,相當於SicThread1
也實現了Runnable
接口,所以也可以寫new Thread(new SicThread1()).start();
這樣的代碼來啓動線程。
也就是說,不管是繼承Thread
類還是實現Runnable
接口,都是利用Thread
類的run
方法。只是前者是重寫了Thread
類的run
方法,後者是給Thread
類傳遞一個Runnable target
,調用target
的run
方法。至於這兩種方法本質上算不算同一種,這就“仁者見仁,智者見智”了。既然Oracle認爲是兩種,那還是以官方描述爲準。
那這兩種方式,哪一種更好?
毫無疑問,實現Runnable
接口更好,理由有三:
- 解耦角度:
Runnable
接口只定義了一個抽象方法run
,語義非常明確,就是線程需要執行的任務。而Thread
類除了線程需要執行的任務,還需要維護線程的生命週期、狀態轉換等 - 資源角度:繼承
Thread
類的方式,如果想要執行一個任務,必須新建一個線程,執行完成後還要銷燬,開銷非常大;而實現Runnable
接口只需要新建任務,可以做到同一個線程執行多個任務,大大減小了線程創建、銷燬的資源浪費 - 擴展角度:Java不支持多繼承,一個類如果繼承了
Thread
類就不能再繼承別的類,不利於未來的擴展
四個屬性
屬性 | 用途/說明 |
---|---|
ID(Long) | 唯一標識不同的線程 |
Name(char[]) | 線程名稱,用於調試 、定位問題等 |
daemon(boolean) | 是否是守護線程,true表示是守護線程,false表示非守護線程(用戶線程) |
priority(int) | 用於告訴CPU哪些線程希望被更多的執行,哪些線程希望被更少的執行 |
線程ID
線程ID從1(主線程)開始自增,(程序)不能手動修改。現在看下Thread
類中關於ID的部分:
// 線程ID
private long tid;
public long getId() {
return tid;
}
// jdk1.8.0_101版本,第422行
// 設置線程ID
/* Set thread ID */
tid = nextThreadID();
// 用於生成線程ID
private static long threadSeqNumber;
// 加鎖的自增操作
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
從源碼可以看出ID的兩個特點:
- 從1開始自增
- 不能手動修改
線程Name
看了線程ID相關的源碼後,很容易就總結除了線程ID相關的特點。所以同樣看下Thread
類關於Name的重要操作:
// 不傳入名字時,默認就是"Thread-" + 數字
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
// 用於匿名線程編號
private static int threadInitNumber;
// 加鎖的自增操作
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
// 可以動態設置線程name
public final synchronized void setName(String name) {
// 確定當前線程有修改該線程的權限
checkAccess();
// 設置線程名字
this.name = name.toCharArray();
if (threadStatus != 0) {
// 如果線程不是處於0(線程未啓動)狀態,則不能修改native層的name
setNativeName(name);
}
}
private native void setNativeName(String name);
至此,可以總結出關於線程Name的兩個特點:
- 默認線程名稱是"Thread-" + 數字(從0開始),爲了方便調試,應該給每個線程取一個有意義的名字
- 實例化時如果沒有設置線程Name,之後還可以通過
setName
的方式設置線程Name
守護線程
守護線程的主要作用是爲了給用戶線程提供一系列服務,守護線程有三個特點:
- 線程類型默認繼承自父線程:守護線程創建的線程默認就是守護線程;用戶線程創建的線程默認就是用戶線程,可以通過
setDaemon
方法修改這個屬性 - 守護線程一般由JVM啓動
- 守護線程不影響JVM的退出
守護線程和用戶線程本質上沒有多大區別,最大的區別就是守護線程不影響JVM的退出。
線程優先級
Java中定義的線程優先級有1-10(十個等級,數值越大,優先級越高),默認爲5。雖然Thread
類定義優先級這個功能,但是程序的設計不應該依賴於優先級。究其原因,主要有兩點:
Thread
類中定義的優先級不代表操作系統的優先級,不同的操作系統有不同的優先級定義- 優先級可能被操作系統改變
核心方法
瞭解了Thread
的定義及核心屬性後,再來看看Thread
的核心方法start
、sleep
、join
、yield
。
start方法
啓動一個線程的方式就是調用它的start()
方法,而不是run()
方法。有時也會被問到這樣兩個問題:
同一個線程兩次(多次)調用start方法會怎樣?
啓動一個線程爲什麼不能調用run方法,而是start方法?
看完start
方法的實現,能輕鬆回答這兩個問題,下面是start
方法的實現
public synchronized void start() {
if (threadStatus != 0)
// 如果線程狀態不是“未啓動”,會拋出IllegalThreadStateException異常
// 這裏就回答了上面的第一個問題
throw new IllegalThreadStateException();
// 加入線程組
group.add(this);
// 線程是否已經啓動,啓動後設置成true
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
// 啓動失敗,把線程從線程組中刪除
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
// 真正的啓動線程的方法(native方法)
private native void start0();
根據start
方法的實現可以總結出start
做了哪些邏輯:
- 檢查線程狀態
- 加入線程組
- 調用native方法
start0
通知JVM啓動一個新線程 - 如果啓動失敗,從線程組中刪除線程
再來回顧下Thread
中run
方法的實現:
@Override
public void run() {
if (target != null) {
target.run();
}
}
對比這兩個方法就可看出,start
方法爲線程的啓動做了一系列準備,再去通知JVM啓動一個新線程;而run
方法僅僅是一個普通方法,所以不能啓動一個新線程。
sleep方法
sleep(long millis)
方法的作用是讓線程休眠指定的時間,在指定時間內不佔用CPU資源。sleep
方法的特點有以下幾點:
- 線程處於
TIMED_WAITING
狀態 sleep
期間不佔用CPU資源sleep
期間不釋放鎖(Synchronized鎖和ReentrantLock都不釋放)sleep
方法能響應中斷,檢測到中斷後拋出InterruptedException
,然後清除中斷狀態
join方法
join
的作用是阻塞當前線程,等待加入的線程執行完成後再繼續執行。使用這個方法一定要清楚是哪個線程被阻塞,舉個例子:
/**
* 使用join方法
* @author sicimike
*/
public class ThreadJoinDemo {
public static void main(String[] args) {
Thread thread = new Thread(()->{
// 可以在此適當的休眠,使結果更清晰
System.out.println("sub thread");
});
thread.start();
try {
// join方法
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread");
}
}
執行結果
sub thread
main thread
在主線程中調用thread.join()
,結果子線程先輸出,主線程後輸出。這個結果是確定的,不存在隨機性。這就是join
方法的作用,主線程中調用子線程的join
方法,阻塞的主線程,等待子線程執行完成後,主線程繼續執行。
日常編碼中應該儘量避免使用join
方法,而是使用JDK封裝好的併發工具CountDownLatch
和CyclicBarrier
代替:併發工具三巨頭CountDownLatch、CyclicBarrier、Semaphore使用
接下來看下join
方法的實現:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 未設置超時時間,一直被阻塞
while (isAlive()) {
// 線程處於可用狀態(既不是NEW,也不是TERMINATED)
// 永久阻塞
wait(0);
}
} else {
while (isAlive()) {
// 線程處於可用狀態(既不是NEW,也不是TERMINATED)
// 計算剩餘的阻塞時間
long delay = millis - now;
if (delay <= 0) {
// 阻塞時間已經到了
break;
}
// 阻塞時間未到,阻塞指定時間
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
通過源碼可以看出,Thread
類中的阻塞是通過wait
方法實現的。
值得注意的是:整個方法執行結束也沒有執行notify
或者notifyAll
方法。因爲Thread
類的run
執行結束後,會自動執行notifyAll
方法。這也是Thread
類不適合作爲鎖對象的原因。
join
方法的特點有以下幾點:
- 線程處於
WAITING
或者TIMED_WAITING
狀態 - 底層調用的
wait
方法 - 能響應中斷,檢測到中斷後拋出
InterruptedException
,然後清除中斷狀態
yield方法
yield
方法的作用是釋放CPU時間片,然後重新競爭。該方法不會釋放鎖,也不會改變線程狀態,線程始終處於RUNNABLE
狀態。
停止線程
如何停止線程是一個比較大的話題,之前特意單獨拿出來寫過: 如何優雅的中斷線程 ,此處就不再贅述。
總結
本篇主要深入源碼,結合實例較爲完整的講解了JDK中的線程Thread
類。具體講解的內容有線程的類定義、成員變量/常量、核心屬性、核心方法、線程啓動、線程終止等。
以上便是本篇的全部內容。