Java併發編程:Thread.java源碼解析

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.結語

學如逆水行舟,不進則退。

4.參考博客

Java線程的創建
Java Thread:揭開Run方法被調用的真正面紗

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章