1. 程式人生 > 程式設計 >java多執行緒之Thread建構函式(原始碼分析)

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的。