Java併發程式設計之執行緒篇之執行緒簡介(二)

AndyJennifer發表於2019-08-19

前言

在上一篇文章中Java併發程式設計之執行緒篇-執行緒的由來已經主要講解了執行緒的由來,以及程式與執行緒的關係。接下來我們就繼續講解在Java中執行緒的相關知識。主要內容包括Java構造與啟動執行緒的方式、執行緒優先順序、執行緒的狀態等知識點。希望大家繼續保持一個熱愛學習的心。快來和我一起學習吧。

Java程式中程式和執行緒的關係

在Java中,一個應用程式對應著一個JVM(Java 虛擬機器)例項,一般來說名字預設為java.exe或者javaw.exe(windows下可以通過工作管理員檢視)。Java採用的是單執行緒程式設計模型,即在我們自己的程式中如果沒有主動建立執行緒的話,只會建立一個執行緒,通常稱為主執行緒。但是要注意,雖然只有一個執行緒來執行任務,不代表JVM中只有一個執行緒,JVM例項在建立的時候,同時會建立很多其他的執行緒(比如垃圾收集器執行緒,Finalizer執行緒等)。具體例子如下所示:

public class Main {
    public static void main(String[] args) {
        //獲取當前程式中所有堆疊資訊
        Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
        //列印所有執行緒名稱
        Set<Thread> threads = allStackTraces.keySet();
        for (Thread thread : threads) {
            System.out.println("執行緒名:" + thread.getName());
        }
    }
}
複製程式碼

上述程式碼中,我們通過Thread.getAllStackTraces()獲取當前程式中所有的執行緒資訊,程式輸出結果為如下所示:

執行緒名:Finalizer
執行緒名:Reference Handler
執行緒名:Signal Dispatcher
執行緒名:Common-Cleaner
執行緒名:main
執行緒名:Monitor Ctrl-Break

複製程式碼

構造執行緒

在上文我們已經瞭解了,在Java程式中預設存在的執行緒,那麼現在我們來看看如何在Java中構建相應執行緒。在Java中構造執行緒需要建立Thread物件,該物件在構造時,預設會呼叫Thread類中的init函式來構造執行緒所需要的屬性,如執行緒所屬的執行緒組、優先順序、堆疊大小等。具體程式碼如下所示:

    /**
     * 初始化一個執行緒物件
     *
     * @param g 當前新執行緒所屬的執行緒組
     * @param target 任務物件
     * @param name 當前新執行緒的名稱
     * @param stackSize 當前新執行緒所需要的堆疊大小
     */
    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        //獲取建立當前新執行緒的執行緒,也就是當前執行緒的父執行緒
        Thread parent = currentThread();
        if (g == null) {
            g = parent.getThreadGroup();
        }

        g.addUnstarted();
        this.group = g;

        this.target = target;
        //複用父執行緒的優先順序與daemon屬性
        this.priority = parent.getPriority();
        this.daemon = parent.isDaemon();
        //設定當前新執行緒的名稱
        setName(name);

        init2(parent);

        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
        //給當前新建立的執行緒分配id
        tid = nextThreadID();
    }
複製程式碼

觀察上述步驟,我們可以發現在init函式中有呼叫了init2函式,我們繼續觀察該函式,程式碼如下所示:

    private void init2(Thread parent) {
        //複用父執行緒的類載入器,
        this.contextClassLoader = parent.getContextClassLoader();
        this.inheritedAccessControlContext = AccessController.getContext();
        //複用父執行緒的可繼承的ThreadLocal
        if (parent.inheritableThreadLocals != null) {
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(
                    parent.inheritableThreadLocals);
        }
    }
複製程式碼

上述程式碼中,新執行緒複用了父執行緒的類載入器與可繼承的ThreadLocal。對於ThreadLocal,之前我也寫了文章AndroidHandler機制之ThreadLocal。有需要的小夥伴可以看看。這裡我們只需要注意的,執行緒中的inheritableThreadLocals一般不會使用。除非你需要新建立的執行緒複用父執行緒的inheritableThreadLocals。對於類載入器,這裡就不過多介紹了。感興趣的小夥伴可以查閱相關資料。

綜上所述。我們可以知道一個新構造的執行緒物件是與其父執行緒(parent 執行緒)息息相關的。新建立的執行緒會複用父執行緒的。優先順序、類載入器、可繼承的ThreadLocal,同時在構造時,還會為該執行緒分配相應的執行緒id。至此Java構造執行緒的方法已經介紹完畢了。

構造執行緒任務

在Java中建立執行緒任務,一般有兩種方式。第一種繼承Thread類並複寫其run方法。第二種實現Runnable介面。下面分別對這兩種方式進行介紹。具體程式碼如下所示:

public class Main {

    public static void main(String[] args) {
        //使用繼承Thread方式
        MyThreadA myThreadA = new MyThreadA();
        myThreadA.start();
        //使用實現Runnable介面
        Thread thread = new Thread(new MyRunnableB());
        thread.start();
    }

    static class MyThreadA extends Thread {
        @Override
        public void run() {
            System.out.println("MyThreadA執行緒任務已經啟動了”);
        }
    }

    static class MyRunnableB implements Runnable {
        @Override
        public void run() {
            System.out.println("MyRunnableB執行緒任務已經啟動了”);
        }
    }
}
//輸出結果:
MyRunnableB執行緒任務已經啟動了
MyThreadA執行緒任務已經啟動了
複製程式碼

觀察上述程式碼,可以發現不管採用何種方式來構建執行緒任務,我們都需要建立相應的Thread物件。並呼叫其start()方法。start方法的含義是告訴當前Java虛擬機器,當前執行緒已經初始化完畢。如果執行緒規劃器空閒。則立即呼叫對於Thread的run()方法執行相應任務。如果run()方法執行完畢,執行緒也隨之終止。

那實現Runnable介面與繼承Thread來建立執行緒任務之間到底有什麼區別呢?如果我們觀察使用Runnable介面的方式,我們發現其實際呼叫了Thread類的建構函式。如下所示:

 public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }
複製程式碼

該建構函式最終會呼叫我們上文提到的init方法,而該方法最終會將我們傳入的Runabale物件賦值給Thread的target屬性。我們再仔細觀察Thread的run()方法,我們會發現其中有一個判斷。如下所示:

  public void run() {
        if (target != null) {
            target.run();
        }
    }
複製程式碼

那麼現在區別就非常明顯了。實現Runnable介面來建立執行緒任務,主要是為了將具體的任務抽離出來,那麼這樣不僅避免了Thread類的單繼承的侷限性,還更符合物件導向的程式設計思想,同時也降低了執行緒物件和執行緒任務的耦合性。

執行緒的優先順序

在上文構造執行緒章節中,我們曾講過,Java執行緒在構建的時候,會複用父執行緒的優先順序。那優先順序代表著什麼呢?在具體講解優先順序之前,我們需要了解分時作業系統。在現代的作業系統(如:Windows、Linux、Mac OS X等)中基本採用的分時的形式排程執行的執行緒,作業系統會分出一個個時間片,執行緒會分配到若干時間片後,當執行緒的時間片用完之後就會發生執行緒的排程,等待下次分配。具體如下圖所示;

分時作業系統.png

時間片是分時作業系統分配給每個正在執行的程式微觀上的一段CPU時間。

通常狀況下,一個系統中所有的執行緒被分配到的時間片長短並不是相等的,不同的作業系統有著自己的時間片分配規則。而執行緒的優先順序的大小隻是佔了執行緒分配時間片的一個權重。也就是說,不是設定了優先順序越高,執行緒就能一定獲得更多的時間片。

在Java執行緒中,通過一個整形成員變數priority來控制優先順序。優先順序的範圍為1-10。我們在構建執行緒的時候可以通過setPriority(int newPriority)方法來設定優先順序,當然Java中也預設了三個優先順序,分別為MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10)。具體程式碼如下所示:

public class Main {

    public static void main(String[] args) {
        //使用實現Runnable介面
        Thread thread1 = new Thread(new MyRunnableB());
        Thread thread2 = new Thread(new MyRunnableB());
        Thread thread3 = new Thread(new MyRunnableB());
        thread1.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.NORM_PRIORITY);
        thread3.setPriority(Thread.MAX_PRIORITY);

        thread1.start();
        thread2.start();
        thread3.start();
    }

    static class MyRunnableB implements Runnable {
        @Override
        public void run() {
            System.out.println("MyRunnableB執行緒任務已經啟動了”);
        }
    }
}
複製程式碼

Daemon執行緒(守護執行緒)

在上文構造執行緒章節中,我們曾講過,Java執行緒在構建的時候也會複用父執行緒的Daemon屬性。其實在Java中執行緒分為守護執行緒使用者執行緒。所謂守護執行緒是指在程式執行的時候在後臺提供一種通用服務的執行緒,比如垃圾回收執行緒就是一個很稱職的守護者,並且這種執行緒並不屬於程式中不可或缺的部分。因此,當所有的非守護執行緒結束時,程式也就終止了,同時會殺死程式中的所有守護執行緒。反過來說,只要使用者執行緒還在執行,程式就不會終止。具體例子如下所示:

需要注意的是,Daemon屬性需要線上程呼叫start方法之前設定。不能線上程啟動的時候設定。

使用者執行緒與守護執行緒的區別.png

通過觀察上訴程式碼,我們明顯可以看見守護程式,程式最後列印了Process finished with exit code 0,也就是暗示程式結束執行了。而使用使用者程式的程式一直沒有結束,一直在迴圈列印相應資訊,這兩者對比,也就是驗證了我們之前的結論。

最後

站在巨人的肩膀上,才能看的更遠~

  • 《Java併發程式設計的藝術》

相關文章