深入理解Thread構造函數


 

 上一篇快速認識線程

 本文參考汪文君著:Java高並發編程詳解。

1、線程的命名

在構造現成的時候可以為線程起一個名字。但是我們如果不給線程起名字,那線程會有一個怎樣的命名呢?

這里我們看一下Thread的源代碼:

 public Thread(ThreadGroup group, Runnable target) {
        init(group, target, "Thread-" + nextThreadNum(), 0);
    }

    /**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (null, null, name)}.
     *
     * @param   name
     *          the name of the new thread
     */
    public Thread(String name) {
        init(null, null, name, 0);
    }

    /**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (group, null, name)}.
     *
     * @param  group
     *         the thread group. If {@code null} and there is a security
     *         manager, the group is determined by {@linkplain
     *         SecurityManager#getThreadGroup SecurityManager.getThreadGroup()}.
     *         If there is not a security manager or {@code
     *         SecurityManager.getThreadGroup()} returns {@code null}, the group
     *         is set to the current thread's thread group.
     *
     * @param  name
     *         the name of the new thread
     *
     * @throws  SecurityException
     *          if the current thread cannot create a thread in the specified
     *          thread group
     */
    public Thread(ThreadGroup group, String name) {
        init(group, null, name, 0);
    }

如果沒有為線程起名字,那么線程將會以“Thread-”作為前綴與一個自增數字進行組合,這個自增數字在整個JVM進程中將會不斷自增:

如果我們執行以下代碼:

import java.util.stream.IntStream;

public class Test {
    public static void main(String[] args) {
        IntStream.range(0,5).boxed()
        .map(
                i->new Thread(
                        ()->System.out.println(
                                Thread.currentThread().getName()
                                )
                        )
                ).forEach(Thread::start);
    }
}

這里使用無參的構造函數創建了5個線程,並且分別輸出了各自的名字:

其實Thread同樣提供了這樣的構造函數。如下

Thread(Runnable target,String name);
Thread(String name);
Thread(ThreadGroup group,Runnable target,String name);
Thread(ThreadGroup group,Runnable target,String name,long stackSize);
Thread(ThreadGroup group,String name);

下面是實現代碼:

import java.util.stream.IntStream;


public class Test2 {
    private final static String PREFIX="ALEX-";
    public static void main(String[] args) {
        IntStream.range(0,5).mapToObj(Test2::createTHREAD).forEach(Thread::start);
    }
    private static Thread createTHREAD(final int intName) {
        return new Thread(()->System.out.println(Thread.currentThread().getName()),PREFIX+intName);
    }
}

運行效果:

需要注意的是,不論你使用的是默認的命名還是特殊的名字,在線程啟動之后還有一個機會可以對其進行修改,一旦線程啟動,名字將不再被修改,下面是setName源碼:

public final synchronized void setName(String name) {
        checkAccess();
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
        if (threadStatus != 0) {
            setNativeName(name);
        }
    }

2、線程的父子關系

Thread的所有構造函數,最終都會調用一個init,我們截取代碼片段對其分析,不難發現新創建的任何一個線程都會有一個父線程:

 private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();//在這里獲取當前線程作為父線程
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            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);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

上面的代碼中的currentThread()是獲取當前線程,在線程的生命周期中,線程的最初狀態為NEW,沒有執行start方法之前,他只能算是一個Thread的實例,並不意味着一個新的線程被創建,因此currentThread()代表的將會是創建它的那個線程,因此我們可以得出以下結論:

  1. 一個線程的創建肯定是由另一個線程完成的
  2. 被創建線程的父線程是創建它的線程

我們都知道main函數所在的線程是由JVM創建的,也就是main線程,那就意味着我們前面創建的所有線程,其父線程都是main線程。

3、Thread與ThreadGroup

在Thread的構造函數中,可以顯式地指定線程的Group,也就是ThreadGroup。

在Thread的源碼中,我們截取片段。

 SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

通過對源碼的分析,我們不難看出,如果沒指定一個線程組,那么子線程將會被加入到父線程所在的線程組,下面寫一個簡單的代碼來測試一下:

package concurrent.chapter02;

public class ThreadConstruction {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1");
        ThreadGroup group = new ThreadGroup("TestGroup");
        Thread t2 = new Thread(group,"t2");
        ThreadGroup mainThreadGroup = Thread.currentThread().getThreadGroup();
        System.out.println("Main thread belong group:"+mainThreadGroup.getName());
        System.out.println("t1 and main belong the same group:"+(mainThreadGroup==t1.getThreadGroup()));
        System.out.println("t2 thread group not belong main group:"+(mainThreadGroup==t2.getThreadGroup()));
        System.out.println("t2 thread group belong main TestGroup:"+(group==t2.getThreadGroup()));
    }
}

運行結果如下所示:

通過上面的例子,我們不難分析出以下結論:

main 線程所在的ThreadGroup稱為main

構造一個線程的時候如果沒有顯示地指定ThreadGroup,那么它將會和父線程擁有同樣的優先級,同樣的daemon。

 

在這里補充一下Thread和Runnable的關系。

Thread負責線程本身的職責和控制,而runnable負責邏輯執行單元的部分。

4、Thread與JVM虛擬機棧

stacksize

在Thread的構造函數中,可發現有一個特殊的參數,stackSize,這個參數的作用是什么呢?

一般情況下,創建線程的時候不會手動指定棧內存的地址空間字節數組,統一通過xss參數進行設置即可,一般來說stacksize越大,代表正在線程內方法調用遞歸的深度就越深,stacksize越小代表着創建的線程數量越多,當然這個參數對平台的依賴性比較高,比如不同的操作系統,不同的硬件。

在有些平台下,越高的stack設定,可以允許的遞歸深度就越多;反之,越少的stack設定,遞歸深度越淺。

JVM內存結構

雖然stacksize在構造時無需手動指定,但是我們會發現線程和棧內存的關系非常密切,想要了解他們之間到底有什么必然聯系,就需要了解JVM的內存分布機制。

JVM在執行Java程序的時候會把對應的物理內存划分成不同的內存區域,每一個區域都存放着不同的數據,也有不同的創建與銷毀時機,有些分區會在JVM啟動的時候就創建,有些則是在運行時才會創建,比如虛擬機棧,根據虛擬機規范,JVM內存結構如圖所示。

1、程序計數器

無論任何語言,其實最終都說需要由操作系統通過控制總線向CPU發送機器指令,Java也不例外,程序計數器在JVM中所起的作用就是用於存放當前線程接下來將要執行的字節碼指令、分支、循環、跳轉、異常處理等信息。在任何時候,一個處理器只執行其中一個線程的指令,為了能夠在CPU時間片輪轉切換上下文之后順利回到正確的執行位置,每條線程都需要具有一個獨立的程序計數器,各個線程互不影響,因此JVM將此塊內存區域設計成了線程私有的。

2、Java虛擬機棧

這里需要重點介紹內存,因為與線程緊密關聯,與程序計數器內存相類似,Java虛擬機棧也是線程私有的,他的生命周期與線程相同,是在JVM運行時所創建的,在線程中,方法在執行的時候都會創建一個名為stack frame的數據結構,主要用於存放局部變量表、操作棧、動態鏈接,方法出口等信息。

每一個線程在創建的時候,JVM都會認為其創建對應的虛擬機棧,虛擬機棧的大小可以通過-xss來配置,方法的調用是棧幀被壓入和彈出的過程,同等的虛擬機棧如果局部變量表等占用內存越小,則可被壓入的棧幀就會越多,反之則可被壓入的棧幀就會越少,一般將棧幀內存的大小成為寬度,而棧幀的數量稱為虛擬機棧的深度。

3、本地方法棧

Java中提供了調用本地方法的接口(java Native Interface),也就是可執行程序,在線程的執行過程中,經常會碰到調用JNI方法,JVM為本地方法所划分的內存區域便是本地方法棧,這塊內存區域其自由度非常高,完全靠不同的JVM廠商來實現,Java虛擬機規范並未給出強制的規定,同樣他也是線程私有的內存區域。

4、堆內存

堆內存是JVM中最大的一塊內存區域,被所有線程所共享,Java在運行期間創造的所有對象幾乎都放在該內存區域,該內存區域也是垃圾回收器重點照顧的區域,因此有時候堆內存被稱為“GC堆”。堆內存一般會被細分為新生代和老年代,更細致的划分為Eden區,FromSurvivor區和To Survivor區。

5、方法區

方法區也是被多個線程所共享的內存區域,它主要用於存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據,雖然在Java虛擬機規范中,將堆內存划分為對內存的一個邏輯分區,但是它還是經常被稱作“非堆”,有時候也被稱為“持久代”,主要是站在垃圾回收器的角度進行划分,但是這種叫法比較欠妥,在HotSpot JVM中,方法區還會被細划分為持久代和代碼緩存區,代碼緩存區主要用於存儲編譯后的本地代碼(和硬件相關)以及JIT 編譯器生成的代碼,當然不同的JVM會有不同的實現。

6、Java 8 元空間

上述內容大致的介紹了JVM的內存划分,在JDK1.8版本以前的內存大致都是這樣划分的,但是從JDK1.8來,JVM的內存區域發生了一些改變,實際上是持久代內存被徹底刪除,取而代之的是元空間。

 

綜上,虛擬機棧內存是線程私有的,也就是說每一個線程都會占有指定的內存大小,我們粗略的認為一個Java進程的內存大小為:堆內存+線程數量*棧內存。

不管是32位操作系統還是64位操作系統,一個進程最大內存是有限制的。簡單來說 線程的數量和虛擬機棧的大小成反比。

5、守護線程

守護線程是一類比較特殊的線程,一般用於處理一些后台的工作,比如JDK的垃圾回收線程。

JVM在什么情況下會退出。

在正常情況下,JVM中若沒有一個非守護線程,則JVM的進程會退出。

這和操作系統的線程概念如出一轍。

什么是守護線程?我們看下下面的代碼:

public class DaemonThread {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()-> {
            while(true) {
                try {
                    Thread.sleep(1);
                }catch(Exception e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        Thread.sleep(2_000L);
        System.out.println("Main thread finished lifestyle");
    }
}

執行這段代碼之后,我們會發現,JVM永遠不會結束。

package concurrent.chapter02;

public class DaemonThread {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()-> {
            while(true) {
                try {
                    Thread.sleep(1);
                }catch(Exception e) {
                    e.printStackTrace();
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        Thread.sleep(2_000L);
        System.out.println("Main thread finished lifestyle");
    }
}

我們加了個thread.setDaemon(true)之后,程序就在main結束后正常推出了。

注意:

設置守護線程的方法很簡單,調用setDaemon方法即可,true代表守護線程,false代表正常線程。

線程是否為守護線程和他的父線程有很大的關系,如果父線程是正常的線程,則子線程也是正常線程,反之亦然,如果你想要修改他的特性則可借助setDaemon方法。isDaemon方法可以判斷該線程是不是守護線程。

另外要注意的是,setDaemon方法旨在線程啟動之前才能生效,如果一個線程已經死亡,那么再設置setDaemon就會拋出IllegalThreadStateException異常。

守護線程的作用:

在了解了什么是守護線程以及如何創建守護線程之后,我們來討論一下為什么要有守護線程,以及何時使用守護線程。

通過上面的分析,如果一個JVM進程中沒有一個非守護線程,那么JVM就會退出,就是說守護線程具備自動結束生命周期的特性,而非守護線程則不具備這個特點,試想一下弱國JVM進程的垃圾回收線程是非守護線程,如果main線程完成了工作,則JVM無法退出,因為垃圾回收線程還在正常的工作。再比如有一個簡單的游戲程序,其中有一個線程正在與服務器不斷地交互以獲得玩家最新的金幣,武器信息,若希望在退出游戲客戶端的時候,這些數據的同步工作也能夠立即結束等等。

守護線程經常用作與執行一些后台任務,因此有時稱他為后台線程,當你希望關閉某些線程的時候,這些數據同步的工作也能夠立即結束,等等。

守護線程經常用作執行一些后台任務,因此有時它也被稱為后台線程,當你希望關閉這些線程的時候,或者退出JVM進程的時候,一些線程能夠自動關閉,此時就可以考慮用守護線程為你完成這樣的工作。

總結:

學習了Thread的構造函數,能夠理解線程與JVM內存模型的關系,還明白了什么是守護線程。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM