1. 程式人生 > >JDK原始碼那些事兒之淺析Thread上篇

JDK原始碼那些事兒之淺析Thread上篇

JAVA中多執行緒的操作對於初學者而言是比較難理解的,其實聯想到底層作業系統時我們可能會稍微明白些,對於程式而言最終都是硬體上執行二進位制指令,然而,這些又太過底層,今天來看一下JAVA中的執行緒,淺析JDK原始碼中的Thread類,之後能幫助我們更好的處理執行緒問題

前言



JDK版本號:1.8.0_171

在Thread註釋中可以看到大佬對其進行的解釋:

Thread就是程式中一個執行緒的執行.JVM允許一個應用中多個執行緒併發執行

每個執行緒都有優先順序.高優先順序執行緒優先於低優先順序執行緒執行
每個執行緒都可以(不可以)被標記為守護執行緒
當執行緒中的run()方法程式碼裡面又建立了一個新的執行緒物件時,新建立的執行緒優先順序和父執行緒優先順序一樣
當且僅當父執行緒為守護執行緒時,新建立的執行緒才會是守護執行緒

當JVM啟動時,通常會有唯一的一個非守護執行緒(這一執行緒用於呼叫指定類的main()方法)
JVM會持續執行執行緒直到下面某一個情況發生為止:
1.類執行時exit()方法被呼叫且安全機制允許此exit()方法的呼叫.
2.所有非守護型別的執行緒均已經終止,或者run()方法呼叫返回或者在run()方法外部丟擲了一些可傳播性的異常.

可以聯想下JVM的啟動過程,從main方法啟動,可以自己寫程式碼檢視下執行緒情況

執行緒實現

Thread註釋類上清楚的寫明瞭執行緒的兩種實現方式:

  • 定義一個繼承Thread類的子類,子類可覆寫父類的run()方法
  • 實現Runnable介面

示例如下,相信各位讀者應該非常熟悉了

    public static void main(String[] args) {
        // 1.繼承Thread類
        ExtendsThread extendsThread = new ExtendsThread("test1");
        extendsThread.start();
        // 2.實現Runnable介面
        ImplementsRunnable implementsRunnable = new ImplementsRunnable("test2");
        // 實現Runnable介面的類不能單獨使用,最終還是要依賴Thread
        Thread implementsRunnableThread = new Thread(implementsRunnable);
        implementsRunnableThread.start();
    }

    static class ExtendsThread extends Thread{

        private String name;

        public ExtendsThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("ExtendsThread is " + name);
        }
    }

    static class ImplementsRunnable implements Runnable{

        private String name;

        public ImplementsRunnable(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("ImplementsRunnable is " + name);
        }
    }

從上面的實現中我們能看到最終都需要實現Runnable介面,也就是實現run方法,Thread本身也是實現了Runnable介面(可參考原始碼部分)

Runnable

先看下Runnable介面實現:

public interface Runnable {
    public abstract void run();
}

非常簡單,就一個run方法,也就意味著我們只要將自定義程式碼邏輯在run方法中實現即可,我們在平常的執行緒實現中最簡單的使用方式也就是如上面執行緒實現的方式,所以我們最終需要了解的部分還是在於Thread,接下來就看看本文的重點Thread

類定義

public class Thread implements Runnable


初始化

先了解下static塊的部分,這裡在類首次載入時執行,主要作用就是將C/C++中的方法對映到Java中的native方法,實現方法命名的解耦

通俗點說就是我們在之後呼叫native方法時,jvm會直接去呼叫本地系統方法完成操作,不會再去查詢這個方法在哪,再去連結等等,相當於先進行註冊,之後就直接操作使用,因為涉及到底層c語言,這裡不過多深入解釋

    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

內部介面/類

Thread內部實現了一些介面和類,下面來一一進行說明

UncaughtExceptionHandler

未捕獲異常處理器,線上程由於未捕獲的異常終止時,JVM會進行一些處理,處理流程如下:

  • JVM呼叫終止執行緒的getUncaughtExceptionHandler方法獲取終止執行緒的uncaughtExceptionHandler
  • 非null則呼叫uncaughtExceptionHandler的uncaughtException方法,同時將此終止執行緒和其異常作為引數傳入
  • null則找到終止執行緒所在的最上級執行緒組,呼叫其uncaughtException方法,同時將此終止執行緒和其異常作為引數傳入
  • 呼叫Thread.getDefaultUncaughtExceptionHandler獲取handle,非空則呼叫其uncaughtException方法,空則判斷呼叫e.printStackTrace(System.err)處理

整個呼叫處理過程可參考下列原始碼部分:

    // 內部異常處理介面
    public interface UncaughtExceptionHandler {

        void uncaughtException(Thread t, Throwable e);
    }
    
    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        // 返回handle本身或執行緒組(實現了這個介面)
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }
    
    // ThreadGroup中程式碼邏輯
    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                // 預設handle非空則呼叫
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                // 未設定基本都是這裡進行列印堆疊資訊
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

可參考下列測試程式碼理解:

    public static void main(String[] args) {
        // 實現UncaughtExceptionHandler介面
        Thread.UncaughtExceptionHandler handler = (t, e) -> {
            System.out.println("test:" + t.getName());
            e.printStackTrace();
        };
        // 設定預設UncaughtExceptionHandler
        // Thread.setDefaultUncaughtExceptionHandler(handler);
        ExtendsThread extendsThread = new ExtendsThread("test");
        // 設定UncaughtExceptionHandler
        extendsThread.setUncaughtExceptionHandler(handler);
        extendsThread.start();
    }

    static class ExtendsThread extends Thread {

        private String name;

        public ExtendsThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            int a = 1 / 0;
            System.out.println(a);
        }
    }

從這個介面我們可以知道對於執行緒異常停止我們其實是可以進行一些後續操作的,通過實現UncaughtExceptionHandler介面來進行執行緒異常後續處理操作,不過我還沒見過有什麼原始碼中用過,一般都是直接catch中處理,這裡就先了解下即可

Caches/WeakClassKey

我們可以看到WeakClassKey這個內部類繼承了WeakReference,而WeakClassKey被Caches所使用,從名字我們也能明白其部分含義,本地快取,WeakClassKey是弱引用相關類,至於弱引用的使用大家可以自行Google,這裡不多說,如果你看過《深入理解Java虛擬機器》,應該多少有點了解

subclassAudits提供了一個雜湊表快取,該快取的鍵型別為java.lang.Thread.WeakClassKey,注意看它的值型別是一個java.lang.Boolean型別的,從其程式碼註釋可以知道這個雜湊表快取中儲存的是所有子類的程式碼執行安全性檢測結果
  
subclassAuditsQueue定義了一個Queue佇列,儲存已經稽核過的子類弱引用

    /** cache of subclass security audit results */
    /* Replace with ConcurrentReferenceHashMap when/if it appears in a future
     * release */
    private static class Caches {
        /** cache of subclass security audit results */
        // 快取安全檢查結果
        static final ConcurrentMap<WeakClassKey,Boolean> subclassAudits =
            new ConcurrentHashMap<>();
        // 佇列
        /** queue for WeakReferences to audited subclasses */
        static final ReferenceQueue<Class<?>> subclassAuditsQueue =
            new ReferenceQueue<>();
    }

    /**
     *  Weak key for Class objects.
     **/
    static class WeakClassKey extends WeakReference<Class<?>> {
        /**
         * saved value of the referent's identity hash code, to maintain
         * a consistent hash code after the referent has been cleared
         */
        private final int hash;

        /**
         * Create a new WeakClassKey to the given object, registered
         * with a queue.
         */
        WeakClassKey(Class<?> cl, ReferenceQueue<Class<?>> refQueue) {
            super(cl, refQueue);
            hash = System.identityHashCode(cl);
        }

        /**
         * Returns the identity hash code of the original referent.
         */
        @Override
        public int hashCode() {
            return hash;
        }

        /**
         * Returns true if the given object is this identical
         * WeakClassKey instance, or, if this object's referent has not
         * been cleared, if the given object is another WeakClassKey
         * instance with the identical non-null referent as this one.
         */
        @Override
        public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (obj instanceof WeakClassKey) {
                Object referent = get();
                return (referent != null) &&
                       (referent == ((WeakClassKey) obj).get());
            } else {
                return false;
            }
        }
    }

在原始碼中我們可以看到主要在isCCLOverridden中使用到了,這裡直接看下這個方法的原始碼,註釋上也寫明瞭,驗證例項(可能是子類)沒有違反安全約束可以被構建,子類不能重寫安全相關的非final方法,否則需要檢查enableContextClassLoaderOverride執行權,主要是進行安全檢查的,簡單理解就好

    /**
     * Verifies that this (possibly subclass) instance can be constructed
     * without violating security constraints: the subclass must not override
     * security-sensitive non-final methods, or else the
     * "enableContextClassLoaderOverride" RuntimePermission is checked.
     */
    private static boolean isCCLOverridden(Class<?> cl) {
        if (cl == Thread.class)
            return false;

        processQueue(Caches.subclassAuditsQueue, Caches.subclassAudits);
        // 生成key
        WeakClassKey key = new WeakClassKey(cl, Caches.subclassAuditsQueue);
        // 從快取查詢
        Boolean result = Caches.subclassAudits.get(key);
        if (result == null) {
            result = Boolean.valueOf(auditSubclass(cl));
            Caches.subclassAudits.putIfAbsent(key, result);
        }
        // 返回結果
        return result.booleanValue();
    }

State

執行緒狀態列舉類,表明執行緒處於生命週期中的哪個階段,執行緒在任意時刻只會處於下列其中一種狀態,只反映JVM中的執行緒狀態,不是反映作業系統執行緒狀態

    public enum State {
        /**
         * 初始態
         * 執行緒建立完畢還未啟動,未呼叫start方法
         */
        NEW,

        /**
         * 包含兩種狀態
         * 1.就緒態
         * 2.執行態
         */
        RUNNABLE,

        /**
         * 阻塞態
         * synchronized會導致執行緒進入blocked狀態
         */
        BLOCKED,

        /**
         * 
         * 等待態
         * 3種情況呼叫後會導致執行緒處於這個狀態:
         * 1.Object.wait
         * 2.Thread.join
         * 3.LockSupport.park
         * 
         * 等待態的執行緒會等待其他執行緒執行特定的操作
         * 
         * 例如一個執行緒呼叫了Object.wait之後進入等待態,另一個執行緒呼叫Object.notify或Object.notifyAll可以將其喚醒,被喚醒的執行緒需要獲取物件的鎖才能恢復執行
         * 呼叫Thread.join會等待指定的執行緒終止
         */
        WAITING,

        /**
         * 
         * 超時等待態
         * 執行緒等待指定的時間再執行
         * 5種情況呼叫後會導致執行緒處於這個狀態:
         * 1.Thread.sleep
         * 2.Object.wait 傳入等待時長
         * 3.Thread.join 傳入等待時長
         * 4.LockSupport.parkNanos
         * 5.LockSupport.parkUntil
         */
        TIMED_WAITING,

        /**
         * 終止態
         * 執行緒執行完畢
         */
        TERMINATED;
    }

執行緒狀態轉化

執行緒狀態轉化可參考下圖理解



在Java中執行緒分為6種狀態,上面列舉類也已經說明了,這裡稍微詳細點說明下每種狀態的含義:

初始態(NEW):

  • 建立一個Thread物件,但還未呼叫start()啟動執行緒時,執行緒處於初始態

執行態(RUNNABLE):

執行態在Java中包括就緒態和執行態

1.就緒態:

  • 該狀態下的執行緒已經獲得執行所需的所有資源,只要CPU分配執行權就能執行
  • 所有就緒態的執行緒存放在就緒佇列中

2.執行態:

  • 獲得CPU執行權,正在執行的執行緒
  • 由於一個CPU同一時刻只能執行一條執行緒,因此每個CPU每個時刻只有一個執行態的執行緒

阻塞態(BLOCKED)

  • 當一條正在執行的執行緒請求某一資源失敗時,就會進入阻塞態
  • 而在Java中,阻塞態專指請求鎖失敗時進入的狀態
  • 由一個阻塞佇列存放所有阻塞態的執行緒。處於阻塞態的執行緒會不斷請求資源,一旦請求成功,就會進入就緒佇列,等待執行

等待態(WAITING)

  • 當前執行緒中呼叫wait、join、park函式時,當前執行緒就會進入等待態
  • 也有一個等待佇列存放所有等待態的執行緒
  • 執行緒處於等待態表示它需要等待其他執行緒的指示才能繼續執行
  • 進入等待態的執行緒會釋放CPU執行權,並釋放資源(如:鎖)

超時等待態(TIMED_WAITING)

  • 當執行中的執行緒呼叫sleep(time)、wait、join、parkNanos、parkUntil時,就會進入該狀態
  • 它和等待態一樣,並不是因為請求不到資源,而是主動進入,並且進入後被其他執行緒喚醒或超時自動喚醒
  • 進入該狀態後釋放CPU執行權和佔有的資源,其中wait()方法會釋放CPU執行權和佔有的鎖,sleep(long)方法僅釋放CPU使用權,鎖仍然佔用
  • 與等待態的區別:到了超時時間後自動進入阻塞佇列,開始競爭鎖

終止態(TERMINATED)

  • 執行緒執行結束後的狀態

其中有幾點需要注意的:

  • yield方法僅釋放CPU執行權,鎖仍然佔用,執行緒會被放入就緒佇列,會在短時間內再次執行
  • wait和notify必須配套使用,即必須使用同一把鎖呼叫
  • wait和notify必須放在一個同步塊中呼叫,wait和notify的物件必須是他們所處同步塊的鎖物件

總結

以上對Thread進行了簡單的介紹說明,對於Thread的狀態轉換需要多理解理解,寫些程式碼debug或者通過jdk工具觀察下jvm的執行緒狀態還是很有必要的

以上內容如有問題歡迎指出,筆者驗證後將及時修正,謝謝

參考

  • Java執行緒中wait狀態和block狀態的區別? (https://www.zhihu.com/question/27654579)