Java併發編程:Thread.java源碼解析
1.前言
最近發現對於線程還不是很熟悉只是停於理解,便對Thread.java類做一個系統的研究,並記錄在此。
2.正文
2.1.案例解析
先看如下三個例子:
@Test
public void test03() throws InterruptedException {
new Thread(new MyRunnable()).start();
new MyThread().start();
new Thread(new FutureTask<>(new MyCallable())).start();
}
public static
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("I am MyRunnable.");
}
}
public static
class MyThread extends Thread {
@Override
public void run() {
System.out.println("I am MyThread.");
}
}
public static
class MyCallable implements Callable<Object> {
@Override
public Object call() throws Exception {
System.out.println("I am MyCallable.");
return null;
}
}
執行結果
從案例分析,我們可以瞭解到任何線程的實現方式都離不開Thread.java,由此可以該類的重要性(ps:除非自己再去構造一個Thread.java)。從某種意義上來講,線程的實現方式有三種,分別是:
- 繼承Thread.java,覆蓋run()方法
- 實現Runnable接口
- 實現Callable接口,注意:實現該接口之後,還需要構造一個FutureTask對象,再把FutureTask對象傳入Thread.java
仔細觀察以上三個接口,我們就會發現FutureTask.java其本質是實現了RunnableFuture接口,而RunnableFuture接口又繼承了Runnable接口,所以其本質上是Runnable的一個子集。爲什麼會有這個接口呢?原因很簡單,我們需要該方法的返回值。
2.2.源碼解析
我們先從Thread.java的執行順序來分析,首先需要注意的是Thread.java中有一個靜態代碼塊,如下:
private static native void registerNatives();
static {
registerNatives();
}
注意該方法標記了native,意思就是該方法會通過JNI調用本地方法,例如C/C++的方法。registerNatives()方法主要的作用就是註冊一些本地方法供Thread.java使用,如start0(),stop0() 等等,可以說,所有操作本地線程的本地方法都是由它註冊的。這個方法放在一個static語句塊中,當該類被加載到JVM中的時候,它就會被調用,進而註冊相應的本地方法。而本地方法registerNatives()是定義在Thread.c文件中的。
Thread.c是個很小的文件,它定義了各個操作系統平臺都要用到的關於線程的公用數據和操作,如下:
JNIEXPORT void JNICALL
Java_Java_lang_Thread_registerNatives (JNIEnv *env, jclass cls){ //registerNatives
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
static JNINativeMethod methods[] = {
{"start0", "()V",(void *)&JVM_StartThread}, //start0 方法
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread}, //stop0 方法
{"isAlive","()Z",(void *)&JVM_IsThreadAlive}, //isAlive 方法
{"suspend0","()V",(void *)&JVM_SuspendThread}, //suspend0 方法
{"resume0","()V",(void *)&JVM_ResumeThread}, //resume0 方法
{"setPriority0","(I)V",(void *)&JVM_SetThreadPriority}, //setPriority0 方法
{"yield", "()V",(void *)&JVM_Yield}, //yield 方法
{"sleep","(J)V",(void *)&JVM_Sleep}, //sleep 方法
{"currentThread","()" THD,(void *)&JVM_CurrentThread}, //currentThread 方法
{"countStackFrames","()I",(void *)&JVM_CountStackFrames}, //countStackFrames 方法
{"interrupt0","()V",(void *)&JVM_Interrupt}, //interrupt0 方法
{"isInterrupted","(Z)Z",(void *)&JVM_IsInterrupted}, //isInterrupted 方法
{"holdsLock","(" OBJ ")Z",(void *)&JVM_HoldsLock}, //holdsLock 方法
{"getThreads","()[" THD,(void *)&JVM_GetAllThreads}, //getThreads 方法
{"dumpThreads","([" THD ")[[" STE, (void *)&JVM_DumpThreads}, //dumpThreads 方法
};
Java_Java_lang_Thread_registerNatives()方法是JNI的命名方式,通過約束就可以準確找到並調用本地方法。methods[] 數組,每一行的參數意思如下:
- 第一個爲Java對應的方法名
- 第二個括號裏面爲參數類型,括號外面爲返回值
- 第三個爲C/C++對應的方法地址
有興趣的同學可以去了解一下JNI,JNI PDF著名書籍下載,閱讀後對你理解一些比較底層方法的源碼有幫助。然後,我們再看其構造函數。
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
Thread(Runnable target, AccessControlContext acc) {
init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name) {
init(group, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
init(group, target, name, stackSize);
}
對於上述參數的含義,如下:
- Runnable target 代表 實現Runnable接口的目標對象
- String name 代表 線程名字,如果不設置線程名字,默認"Thread-" + nextThreadNum()
- ThreadGroup group 代表 線程組,沒有設置線程組,默認從父線程繼承
- AccessControlContext acc 代表 權限訪問控制
- long stackSize 代表 該線程請求的堆棧大小,如果創建者沒有指定堆棧大小,則爲0。這取決於VM如何處理這個數字;一些vm會忽略它。
從Thread.java的所有構造函數中,我們可以清晰的發現,所有的構造函數都調用了init()方法。對於構造函數調用實例方法init(),有必要說明一下:
類體中的方法分爲實例方法和類方法兩種,用static修飾的是類方法。
當類的字節碼文件被加載到內存時,類的實例方法不會被分配入口地址,當該類創建對象後,類中的實例方法才分配入口地址,從而實例方法可以被類創建的任何對象調用執行。需要注意的是,當我們創建第一個對象時,類中的實例方法就分配了入口地址,當再創建對象時,不再分配入口地址,也就是說,方法的入口地址被所有的對象共享,當所有的對象都不存在時,方法的入口地址才被取消。
對於類中的類方法,在該類被加載到內存時,就分配了相應的入口地址。從而類方法不僅可以被類創建的任何對象調用執行,也可以直接通過類名調用。類方法的入口地址直到程序退出才被取消。類方法在類的字節碼加載到內存時就分配了入口地址。
因此,Java語言允許通過類名直接調用類方法,而實例方法不能通過類名調用。在Java語言中,類中的類方法不可以操作實例變量,也不可以調用實例方法,這是因爲在類創建對象之前,實例成員變量還沒有分配內存,而且實例方法也沒有入口地址。
實例對象的構造順序是 實例變量>實例代碼塊>構造函數,所以在構造函數中使用實例方法init()是可以的。init()代碼如下:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//檢查線程的name是否爲空,爲空則拋出異常
if (name == null) {
throw new NullPointerException("name cannot be null");
}
//設置線程名字
this.name = name;
//返回對當前正在執行的線程對象的引用
Thread parent = currentThread();
//如果已經爲當前應用程序建立了安全管理員,則返回該安全管理員; 否則返回null
SecurityManager security = System.getSecurityManager();
//檢查線程組
if (g == null) {
//安全檢查
if (security != null) {
//返回要在其被調用時實例化任何正在創建的新線程的線程組。 默認情況下,它返回當前線程的線程組
g = security.getThreadGroup();
}
//設置線程組
if (g == null) {
//當前正在執行的線程對象的線程組
g = parent.getThreadGroup();
}
}
//檢查可達性,確定當前運行的線程是否有權限修改此線程組
g.checkAccess();
//是否有權限訪問
if (security != null) {
//驗證此實例(可能是子類)可以在不違反安全約束的情況下構造:子類必須不覆蓋對安全敏感的非最終方法,
//否則檢查“enableContextClassLoaderOverride”運行時權限。
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();
//設置目標對象
this.target = target;
//設置優先級,優先級覆蓋父線程的優先級
setPriority(priority);
//設置InheritableThreadLocal,提供從父線程到子線程的值繼承:
//當創建子線程時,子線程將接收父線程具有值的所有可繼承線程局部變量的初值。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//存儲指定的堆棧大小
this.stackSize = stackSize;
//設置線程ID
tid = nextThreadID();
}
初始化完成後,就需要顯示調用start()方法,讓其進入準備狀態。start()方法如下:
public synchronized void start() {
//如果不是準備狀態,拋出異常,即一個線程只能使用一次start()方法
if (threadStatus != 0)
throw new IllegalThreadStateException();
//添加進線程組
group.add(this);
//開始標記
boolean started = false;
try {
//調用native方法
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 */
}
}
}
private native void start0();
上述代碼,配上註釋很容易理解。其中start0()方法是會調用本地方法,在本文最開始,我們發現
{"start0", "()V",(void *)&JVM_StartThread}
JVM_StartThread方法將會被Java調用,在 jvm.cpp 中,我們發現:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)) {
JavaThread *native_thread = NULL;
...
native_thread = new JavaThread(&thread_entry, sz);
...
Thread::start(native_thread);
}
在hotspot\src\share\vm\runtime\thread.cpp中查看JavaThread對象的實現:
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : ...
{
...
os::create_thread(this, thr_type, stack_sz);
}
void Thread::start(Thread* thread)
{
...
os::start_thread(thread);
}
在hotspot\src\os目錄下可以看到windows, linux, solaris和posix的實現,先檢查linux\vm\os_linux.cpp:
bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size)
{
...
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);
...
}
通過調用平臺的API創建一個線程!在回到 jvm.cpp 中,代碼段:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)){
...
native_thread = new JavaThread(&thread_entry, sz);
...
}
這裏JVM_ENTRY()是一個宏,用來定義JVM_StartThread()函數,可以看到函數內創建了真正的平臺相關的本地線程,其線程函數是 thread_entry,如下:
static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj());
JavaValue result(T_VOID);
JavaCalls::call_virtual(&result,obj,
KlassHandle(THREAD,SystemDictionary::Thread_klass()),
vmSymbolHandles::run_method_name(), //LOOK! 看這裏
vmSymbolHandles::void_method_signature(),THREAD);
}
可以看到調用了 vmSymbolHandles::run_method_name 方法,而run_method_name是在 vmSymbols.hpp 用宏定義的:
class vmSymbolHandles: AllStatic {
...
template(run_method_name,"run") //LOOK!!! 這裏決定了調用的方法名稱是 “run”!
...
}
就是這裏進行調用了Thread.java類中的run方法。而Thread.java類的run方法如下:
public void run() {
if (target != null) {
target.run();
}
}
很明顯,該方法又去調用target對象的run方法。至此我們理解了一個完整的Thread的創建以及運行過程。後面我將貼出比較重要的一些方法,有興趣可以閱讀源碼。
//返回當前正在運行的線程
public static native Thread currentThread();
//向調度程序發出的提示,表明當前線程願意使用當前的處理器。調度程序可以忽略這個提示
public static native void yield();
//使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行),具體取決於系統定時器和調度程序的精度和準確性
public static native void sleep(long millis) throws InterruptedException;
//測試該線程是否爲活動線程。如果線程已啓動但尚未死亡,則該線程爲活動線程
public final native boolean isAlive();
//當且僅當當前線程持有指定對象上的監視器鎖時,返回true
public static native boolean holdsLock(Object obj);
值得一提的是Thread.java優先級範圍是1-10。如下:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
當線程執行完任務時,如何銷燬呢?答案在exit()方法中:
private void exit() {
if (group != null) {
//通知組線程該線程已經終止。
group.threadTerminated(this);
group = null;
}
target = null;
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
上述代碼,全部對象設置爲空,可以使JVM垃圾回收器有效的進行回收,減少內存使用量。再來看看join()方法,需要提前說明的是Object.java對象的wait()方法,wait()方法使當前線程等待,直到另一個線程調用
notify()或notifyAll()方法或已經過了指定的時間(就算wait的傳參值)。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()) {
wait(0);
}
} else {
//直到超過預期時間
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
對於線程異常的捕獲可以通過setUncaughtExceptionHandler()方法來控制,如下:
//設置在此線程因未捕獲異常而突然終止時調用的處理程序
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
3.結語
學如逆水行舟,不進則退。