java多執行緒之Thread建構函式(原始碼分析)
在上一篇文章中對執行緒狀態生命週期和常見的執行緒api進行了一個講解。這篇文章開始著重對其構造方法進行一個說明,也將揭曉為什麼我們呼叫了start方法就能啟動一個執行緒。
一、守護執行緒和非守護執行緒
我們獲取執行緒的id的時候會發現每次都不是0,這是因為在java虛擬機器器執行一個執行緒的時候會預設啟動一些其他的執行緒,來為我們的執行緒服務。預設建立的和我們自己建立的執行緒是有區分的。這就要區分守護執行緒和非守護執行緒了。
1、什麼是守護執行緒和非守護執行緒?
預設啟動的這些執行緒就是守護執行緒,他專門處理一些後臺的工作。比如說垃圾回收等。非守護執行緒就是我們自己建立的這些執行緒。官方檔案指出,當java虛擬機器器中沒有非守護執行緒了,預設執行緒也會退出。舉個例子就能明白:
守護執行緒就像飯店裡面的服務員,非守護執行緒就像是顧客,顧客沒有了,那麼服務員也沒有存在的必要了。
2、程式碼演示
我們通過程式碼來演示一下他們的作用,
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (true) {
System.out.println("無限迴圈");
}
}) ;
thread.start();
}
}
複製程式碼
在這裡主要有兩個執行緒一個是main執行緒,第二個就是自己建立的thread。執行之後很明顯程式會無線的執行下去,因為thread是非守護執行緒。即使是main執行緒執行結束了thread也會執行。現在我們把thread設定為守護執行緒就不一樣了。
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (true) {
System.out.println("無限迴圈");
}
}) ;
//設定為守護執行緒
thread.setDaemon(true);
thread.start();
}
}
複製程式碼
在執行一遍,我們會發現程式正常的退出了,這是因為我們把thread設定成了守護執行緒,你想想看main執行緒和thread都變成了服務員,現在沒有顧客了,於是這些守護執行緒到店裡轉悠一圈就走了。
3、守護執行緒應用場景?
你瞭解了守護執行緒的特點之後,就可以運用這個原理做一些意想不到的事,比如說在退出jvm的時候也想讓一些執行緒跟著退出,就可以把他設定為守護執行緒。
對這個基本的概念瞭解了之後我們再來看看執行緒的建構函式。
二、執行緒的建構函式
1、建構函式
執行緒Thread得建構函式一共有8個,
在這裡我們接觸到了一個新的類ThreadGroup。它代表的含義就是一個執行緒所屬的執行緒組。在上面我們可以看到在例項化一個執行緒時候,既可以指定執行緒所屬的執行緒組,也可以宣告其runnable介面。下面我們分析一下這個執行緒組ThreadGroup。
他們倆的關係可以這樣表示:
在上面說我們能夠指定執行緒所在的執行緒組,下面我們就程式碼演示一下。
public class Test {
public static void main(String[] args) {
//為當前執行緒設定執行緒組
ThreadGroup group= new ThreadGroup("執行緒組");
Thread thread = new Thread(group,"當前執行緒");
thread.start();
System.out.print(thread.getThreadGroup().getName());
}
}
//輸出:執行緒組
複製程式碼
這就是其基本用法,但是如果我們沒有指定執行緒所屬的執行緒組輸出會是什麼結果呢?測試一下:
public class Test {
public static void main(String[] args) {
ThreadGroup group= new ThreadGroup("執行緒組");
ThreadGroup maingroup = Thread.currentThread().getThreadGroup();
Thread thread1 = new Thread(group,"執行緒A");
Thread thread2 = new Thread("執行緒B");
System.out.println(thread1.getThreadGroup().getName());
System.out.println(thread2.getThreadGroup().getName());
System.out.println(maingroup.getName());
}
}
//輸出:執行緒組 main main
複製程式碼
上面的程式碼的意思是這樣的,執行緒A指定了我們建立的執行緒組,執行緒B預設的執行緒組,maingroup是主執行緒組。根據輸出結果我們會發現,如果一個執行緒沒有指定執行緒組,那麼他就和父親的執行緒組是一樣的。
2、例項化一個執行緒
上面給出了執行緒的八個構造方法,我們可以使用這八個構造方法去例項化一個執行緒,但是底層是如何做的呢?會不會是像普通類那樣例項化的呢?對此我們就需要深入執行緒的原始碼去看看:
public Thread(ThreadGroup group,Runnable target,String name,long stackSize) {
init(group,target,name,stackSize);
}
複製程式碼
我們選用了一個最複雜的構造方法,因為其他構造方法都是其子集,我們可以看到,這裡其實呼叫的是init方法,也就是說真正實現初始化的是在init方法中進行的。我們不妨跟進去看看:
private void init(ThreadGroup g,long stackSize) {
init(g,stackSize,null,true);
}
複製程式碼
這個init方法裡面還有一層,而且還多出了兩個引數。想要搞清楚我們就需要再跟進去看看:
private void init(ThreadGroup g,long stackSize,AccessControlContext acc,boolean inheritThreadLocals) {
//第一部分:確保執行緒名字不為空
if (name == null) {
throw new NullPointerException("name cannot be null");
}
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();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
//第三部分:一些其他引數設定
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
//第四部分:runnable介面配置
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//第五部分:設定棧大小
this.stackSize = stackSize;
//第六部分:設定執行緒ID
tid = nextThreadID();
}
複製程式碼
終於找到了初始化執行緒的方法。我們劃分了五個部分:
(1)第一部分:確保執行緒名字不能為空,
在這裡就不得不提一句執行緒名了,java官方要求我們開發者如果沒有顯示的為執行緒指定一個名字,那麼執行緒將以“Thread-”為字首以數字為字尾,組成執行緒的名字。但是無論如何執行緒都需要有一個名字。
(2)第二部分:指定當前執行緒的執行緒組
裡面的程式碼很明白,也就是說如果g不為空,我們就是用這個g作為當前執行緒的執行緒組,否則的話就使用父類的執行緒組。當然了,中間還要檢查一下許可權問題等等。
(3)第三部分:其他屬性配置
在這裡配置了是否設定為守護執行緒、優先順序、類載入器等。
(4)第四部分:runnable介面配置
指定實現了runnable介面類。
(5)第五部分:設定棧大小
執行緒的棧大小 根據引數傳遞過程可以看出預設大小為零,即使用預設的執行緒棧大小
(6)第六部分:設定執行緒id
執行緒的ID是在nextThreadID方法中指定的。我們可以看看如何指定執行緒的ID的。
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
複製程式碼
可以看到,其實就是threadSeqNumber。
OK,以上就是如何初始化一個執行緒,相信我們都比較清楚了,其他的建構函式只是對這個init方法的引數進行了一些改變而已。但是原理都是一樣的。上篇文章中提到的一個問題還沒有解決,接著往下看。
三、為什麼呼叫start方法就能啟動一個執行緒
為瞭解決這個問題我們還必須要深入原始碼看一下(jdk1.8):
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
複製程式碼
這些程式碼的意思是什麼呢?首先會判斷執行緒狀態是否異常,然後把當前執行緒在啟動之前加入到執行緒組中,最後呼叫start0方法正式的啟動執行緒。現在關鍵來了,真正啟動執行緒的是這個start0方法,我們不妨再追進去看看:
也就是說真正啟動時native方法啟動的,好像也沒有呼叫run方法,為什麼run方法裡面的內容就被執行了呢。官方檔案是這麼解釋的:JNI方法start0內部呼叫了run方法。就是這麼一句話就解釋了上面的這個原因。
上面已經解決了兩個問題,第一個就是建構函式,第二個也理解了為什麼我們呼叫start方法就能啟動一個執行緒而不是run。我們分析原始碼就能知道,執行緒提供的api方法基本上全部是native的。