深刻理解Thread構造函數

 

 上一篇快速認識線程html

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

一、線程的命名

在構造現成的時候能夠爲線程起一個名字。可是咱們若是不給線程起名字,那線程會有一個怎樣的命名呢?編程

這裏咱們看一下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);

下面是實現代碼:app

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);
    }
}

運行效果:less

須要注意的是,不論你使用的是默認的命名仍是特殊的名字,在線程啓動以後還有一個機會能夠對其進行修改,一旦線程啓動,名字將再也不被修改,下面是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);
        }
    }

二、線程的父子關係

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線程。

三、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負責邏輯執行單元的部分。

四、Thread與JVM虛擬機棧

stacksize

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

通常狀況下,建立線程的時候不會手動指定棧內存的地址空間字節數組,統一經過xss參數進行設置便可,通常來講stacksize越大,表明正在線程內方法調用遞歸的深度就越深,stacksize越小表明着建立的線程數量越多,固然這個參數對平臺的依賴性比較高,好比不一樣的操做系統,不一樣的硬件。

在有些平臺下,越高的stack設定,能夠容許的遞歸深度就越多;反之,越少的stack設定,遞歸深度越淺。

JVM內存結構

雖然stacksize在構造時無需手動指定,可是咱們會發現線程和棧內存的關係很是密切,想要了解他們之間到底有什麼必然聯繫,就須要瞭解JVM的內存分佈機制。

JVM在執行Java程序的時候會把對應的物理內存劃分紅不一樣的內存區域,每個區域都存放着不一樣的數據,也有不一樣的建立與銷燬時機,有些分區會在JVM啓動的時候就建立,有些則是在運行時纔會建立,好比虛擬機棧,根據虛擬機規範,JVM內存結構如圖所示。

一、程序計數器

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

二、Java虛擬機棧

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

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

三、本地方法棧

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

四、堆內存

堆內存是JVM中最大的一塊內存區域,被全部線程所共享,Java在運行期間創造的全部對象幾乎都放在該內存區域,該內存區域也是垃圾回收器重點照顧的區域,所以有時候堆內存被稱爲「GC堆」。堆內存通常會被細分爲新生代和老年代,更細緻的劃分爲Eden區,FromSurvivor區和To Survivor區。

五、方法區

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

六、Java 8 元空間

上述內容大體的介紹了JVM的內存劃分,在JDK1.8版本之前的內存大體都是這樣劃分的,可是從JDK1.8來,JVM的內存區域發生了一些改變,其實是持久代內存被完全刪除,取而代之的是元空間。

 

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

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

五、守護線程

守護線程是一類比較特殊的線程,通常用於處理一些後臺的工做,好比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內存模型的關係,還明白了什麼是守護線程。

相關文章
相關標籤/搜索