Java併發編程之線程篇之線程簡介(二)

前言

在上一篇文章中Java併發編程之線程篇-線程的由來已經主要講解了線程的由來,以及進程與線程的關係。接下來咱們就繼續講解在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()獲取當前程序中全部的線程信息,程序輸出結果爲以下所示:windows

線程名:Finalizer
線程名:Reference Handler
線程名:Signal Dispatcher
線程名:Common-Cleaner
線程名:main
線程名:Monitor Ctrl-Break

複製代碼

構造線程

在上文咱們已經瞭解了,在Java程序中默認存在的線程,那麼如今咱們來看看如何在Java中構建相應線程。在Java中構造線程須要建立Thread對象,該對象在構造時,默認會調用Thread類中的init函數來構造線程所須要的屬性,如線程所屬的線程組、優先級、堆棧大小等。具體代碼以下所示:markdown

/** * 初始化一個線程對象 * * @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。對於類加載器,這裏就不過多介紹了。感興趣的小夥伴能夠查閱相關資料。ide

綜上所述。咱們能夠知道一個新構造的線程對象是與其父線程(parent 線程)息息相關的。新建立的線程會複用父線程的。優先級、類加載器、可繼承的ThreadLocal,同時在構造時,還會爲該線程分配相應的線程id。至此Java構造線程的方法已經介紹完畢了。函數

構造線程任務

在Java中建立線程任務,通常有兩種方式。第一種繼承Thread類並複寫其run方法。第二種實現Runnable接口。下面分別對這兩種方式進行介紹。具體代碼以下所示:post

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類的構造函數。以下所示:this

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併發編程的藝術》
相關文章
相關標籤/搜索