本文參考汪文君著: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()代表的將會是創建它的那個線程,因此我們可以得出以下結論:
- 一個線程的創建肯定是由另一個線程完成的
- 被創建線程的父線程是創建它的線程
我們都知道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內存模型的關系,還明白了什么是守護線程。