上一篇快速認識線程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()表明的將會是建立它的那個線程,所以咱們能夠得出如下結論:
咱們都知道main函數所在的線程是由JVM建立的,也就是main線程,那就意味着咱們前面建立的全部線程,其父線程都是main線程。
在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的構造函數中,可發現有一個特殊的參數,stackSize,這個參數的做用是什麼呢?
通常狀況下,建立線程的時候不會手動指定棧內存的地址空間字節數組,統一經過xss參數進行設置便可,通常來講stacksize越大,表明正在線程內方法調用遞歸的深度就越深,stacksize越小表明着建立的線程數量越多,固然這個參數對平臺的依賴性比較高,好比不一樣的操做系統,不一樣的硬件。
在有些平臺下,越高的stack設定,能夠容許的遞歸深度就越多;反之,越少的stack設定,遞歸深度越淺。
雖然stacksize在構造時無需手動指定,可是咱們會發現線程和棧內存的關係很是密切,想要了解他們之間到底有什麼必然聯繫,就須要瞭解JVM的內存分佈機制。
JVM在執行Java程序的時候會把對應的物理內存劃分紅不一樣的內存區域,每個區域都存放着不一樣的數據,也有不一樣的建立與銷燬時機,有些分區會在JVM啓動的時候就建立,有些則是在運行時纔會建立,好比虛擬機棧,根據虛擬機規範,JVM內存結構如圖所示。
不管任何語言,其實最終都說須要由操做系統經過控制總線向CPU發送機器指令,Java也不例外,程序計數器在JVM中所起的做用就是用於存放當前線程接下來將要執行的字節碼指令、分支、循環、跳轉、異常處理等信息。在任什麼時候候,一個處理器只執行其中一個線程的指令,爲了可以在CPU時間片輪轉切換上下文以後順利回到正確的執行位置,每條線程都須要具備一個獨立的程序計數器,各個線程互不影響,所以JVM將此塊內存區域設計成了線程私有的。
這裏須要重點介紹內存,由於與線程緊密關聯,與程序計數器內存相相似,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會有不一樣的實現。
上述內容大體的介紹了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內存模型的關係,還明白了什麼是守護線程。