1 線程的生命周期
每個(gè)線程都有自己的局部變量表帖世、程序計(jì)數(shù)器以及生命周期。
上圖就時(shí)一個(gè)線程的生命周期圖沸枯,答題可以分為5個(gè)主要階段:
- NEW
- RUNNABLE
- RUNNING
- BLOCKED
- TERMINATED
1.1 NEW狀態(tài)
當(dāng)我們new一個(gè)Thread對(duì)象時(shí)日矫,此時(shí)它并不處于執(zhí)行狀態(tài),因?yàn)闆]有調(diào)用start方法绑榴,那么線程的狀態(tài)為NEW狀態(tài)哪轿。
NEW狀態(tài)通過start方法進(jìn)入RUNNABLE狀態(tài)。
1.2 RUNNABLE狀態(tài)
線程對(duì)象進(jìn)入RUNNABLE狀態(tài)必須調(diào)用start方法翔怎,那么此時(shí)才是真正地在JVM進(jìn)程中創(chuàng)建了一個(gè)線程窃诉。
線程一啟動(dòng)就可以立即得到執(zhí)行嗎?
不一定赤套,線程的運(yùn)行與否和進(jìn)程一樣都要聽令于CPU的調(diào)度飘痛,那么我們把這個(gè)中間狀態(tài)稱為可執(zhí)行狀態(tài)(RUNNABLE),也即是說它具備執(zhí)行的資格,但是并沒有真正地執(zhí)行起來二十在等待CPU的調(diào)度容握。
嚴(yán)格意義上講宣脉,RUNNABLE的線程只能意外終止或者進(jìn)入RUNNING狀體。
1.3 RUNNING狀態(tài)
一旦CPU通過輪詢或者其它方式從任務(wù)可執(zhí)行隊(duì)列中選中了線程剔氏,那么此時(shí)它才能真正地執(zhí)行自己地邏輯代碼脖旱,需要說明的一點(diǎn)是一個(gè)正在RUNNING狀態(tài)的線程事實(shí)上也是RUNNABLE的堪遂,但是反過來則不成立。
在該狀態(tài)下萌庆,線程的狀態(tài)可以發(fā)生如下的狀態(tài)轉(zhuǎn)換:
- 直接進(jìn)入TERMINATED狀態(tài),比如調(diào)用已不推薦的stop方法或者是判斷某個(gè)邏輯標(biāo)志
- 進(jìn)入BLOCKED狀態(tài)币旧,比如調(diào)用了sleep或者wait方法而加入了waitSet中
- 進(jìn)行某個(gè)阻塞的IO操作践险,比如因網(wǎng)絡(luò)數(shù)據(jù)的讀寫進(jìn)入了BLOCKED狀態(tài)
- 獲取某個(gè)鎖資源,從而加入到該鎖的阻塞隊(duì)列中進(jìn)入了BLOCKED狀態(tài)吹菱。
- 由于CPU的調(diào)度器輪詢使該線程放棄執(zhí)行巍虫,進(jìn)入RUNNABLE狀態(tài)。
- 線程主動(dòng)調(diào)用yield方法鳍刷,放棄CPU執(zhí)行權(quán)占遥,進(jìn)入RUNNABLE狀態(tài)
1.4 BLOCKED狀態(tài)
線程在BLOCKED狀態(tài)中可以切換如下幾個(gè)狀態(tài):
- 直接進(jìn)入TERMINATED狀態(tài),比如調(diào)用JDK不推薦使用的stop方法或者意外死亡(JVM Crash)
- 線程阻塞的操作結(jié)束输瓜,比如讀取了想要的數(shù)據(jù)字節(jié)進(jìn)入到RUNNABLE狀態(tài)
- 線程完成了指定時(shí)間的休眠瓦胎,進(jìn)入到了RUNNABLE狀態(tài)
- wait中的線程被其它線程notify/notifyAll喚醒,進(jìn)入RUNNABLE狀態(tài)
- 線程獲取到了某個(gè)鎖資源尤揣,進(jìn)入RUNNABLE狀態(tài)
- 線程在阻塞過程中被打斷搔啊,比如其它線程調(diào)用 interrupt方法,進(jìn)入RUNNABLE狀態(tài)北戏。
1.5 TERMINATED狀態(tài)
TERMINATED是一個(gè)線程的最終狀態(tài)负芋,在該狀態(tài)中線程將不會(huì)切換到其它任何狀態(tài),線程進(jìn)入TERMINATED狀態(tài)嗜愈,意味著該線程的整個(gè)生命周期都結(jié)束了旧蛾,下列情況下將會(huì)使線程進(jìn)入TERMINATED狀態(tài)。
- 線程運(yùn)行正常結(jié)束蠕嫁,結(jié)束生命周期
- 線程運(yùn)行出錯(cuò)意外結(jié)束
- JVM Crash,導(dǎo)致所有的線程都結(jié)束锨天。
2 線程的start方法
可以從源碼知道,start方法首先會(huì)判斷線程狀態(tài)拌阴,如過不在符合條件的狀態(tài)會(huì)拋異常绍绘,然后會(huì)調(diào)用start0()一個(gè)jni方法,然后jni方法會(huì)調(diào)用Thread的run方法迟赃,從而使run方法里面的邏輯業(yè)務(wù)可以執(zhí)行陪拘。
- Thread被構(gòu)造后處于New狀態(tài),實(shí)際上它的內(nèi)部屬性為0.
- 不能兩次啟動(dòng)Thread纤壁,否則會(huì)拋出
IlleagalThreadStateException
- 線程啟動(dòng)后將被加入到一個(gè)ThreadGroup中
- 線程處于
TERMINAL
狀態(tài)左刽,是無法回到RUNNABLE
/RUNNING
狀態(tài)的。
2.1 模板設(shè)計(jì)模式在Thread中的應(yīng)用
線程真正執(zhí)行的邏輯實(shí)在run
方法里酌媒,通常我們會(huì)把run方法成為線程的執(zhí)行單元欠痴。其實(shí)Thread里的start和run方法就是一個(gè)比較典型的模板設(shè)計(jì)模式迄靠,父類編寫算法數(shù)據(jù)結(jié)果,子類負(fù)責(zé)實(shí)現(xiàn)具體細(xì)節(jié)喇辽,另外Runnalbe接口的使用也可以將線程的控制和業(yè)務(wù)邏輯的執(zhí)行徹底分開來掌挚。
2.2 Runnable接口
創(chuàng)建線程只有通過構(gòu)造方法構(gòu)建,而實(shí)現(xiàn)線程具體的業(yè)務(wù)執(zhí)行單元有兩種菩咨,一種是重寫run方法吠式,另一種是實(shí)現(xiàn)Runnable接口里的run方法,并且將Runnable的實(shí)例傳給Thread構(gòu)造方法當(dāng)參數(shù)抽米。
3 Thread構(gòu)造方法
3.1 線程的命名
如果沒有顯式地為線程指定名字特占,線程會(huì)以Thread-
作為前綴和一個(gè)自增數(shù)字進(jìn)行組合,這個(gè)自增數(shù)字會(huì)不斷在JVM中進(jìn)行自增云茸。
3.2 命名線程
強(qiáng)烈建議構(gòu)造線程時(shí)自定義線程名是目,通常都是自定義前綴加數(shù)字命名。
注意:線程為未啟動(dòng)之前可以通過setName()
方法更改線程名标捺,如果已啟動(dòng)懊纳,則無法更改。
3.3 線程的父子關(guān)系
- 一個(gè)線程的創(chuàng)建肯定是在另一個(gè)線程完成的
- 被創(chuàng)建線程的父線程就是創(chuàng)建它的線程
3.4 Thread和ThreadGroup
從源碼可以看出宜岛,如果在構(gòu)造器未指定線程組长踊,那么該線程會(huì)默認(rèn)加入到父線程所在的線程組中。
- main線程所在的線程組叫
main
- 構(gòu)造一個(gè)線程時(shí)如果沒有顯示指定一個(gè)ThreadGroup萍倡,那么它將會(huì)和父線程同屬于一個(gè)ThreadGroup
3.5 Thread和Runnable
Thread負(fù)責(zé)線程本身的職責(zé)和控制身弊,Runnable負(fù)責(zé)邏輯執(zhí)行單元
3.6 Thread和JVM虛擬機(jī)棧
在構(gòu)造方法中,我們可以發(fā)現(xiàn)一個(gè)stackSize參數(shù)列敲。一般情況下阱佛,創(chuàng)建線程的時(shí)候不會(huì)手動(dòng)指定棧內(nèi)存空間的地址空間字節(jié)數(shù)組,同意用xss參數(shù)設(shè)置即可戴而,在某些平臺(tái)下凑术,越高的stack設(shè)定,可以允許的遞歸深度越高所意;反之淮逊,越少的stack設(shè)定,則遞歸深度越淺扶踊。不過這個(gè)參數(shù)很依賴平臺(tái)泄鹏。
3.7 JVM內(nèi)存結(jié)構(gòu)
3.7.1 程序計(jì)數(shù)器
無論任何語言,其實(shí)最終都是需要操作系統(tǒng)通過控制總線向CPU發(fā)送機(jī)器指令秧耗,Java也不例外备籽,程序計(jì)數(shù)器在JVM中所起的作用就是用于存放當(dāng)前線程接下來將要執(zhí)行的字節(jié)碼指令、分支分井、循環(huán)车猬、跳轉(zhuǎn)霉猛、異常處理等信息。在任何時(shí)候珠闰,一個(gè)處理器只處理其中一個(gè)線程中的指令惜浅,為了能夠在CPU時(shí)間片輪轉(zhuǎn)切換上下文順利回到正確的執(zhí)行位置,每個(gè)線程都需要具有一個(gè)獨(dú)立的程序計(jì)數(shù)器伏嗜,各個(gè)線程之間互不影響赡矢,因此JVM將此塊內(nèi)存區(qū)域設(shè)計(jì)了線程私有。
3.7.2 java虛擬機(jī)棧
與程序計(jì)數(shù)器內(nèi)存一樣阅仔,Java虛擬機(jī)棧也是線程私有的,它的生命周期和線程一樣弧械,也是在jvm運(yùn)行時(shí)創(chuàng)建的八酒,在線程中,方法在執(zhí)行的時(shí)候會(huì)在創(chuàng)建一個(gè)叫棧幀的數(shù)據(jù)結(jié)構(gòu)刃唐,主要用于存放方法的局部變量表羞迷,操作棧,動(dòng)態(tài)鏈接画饥,方法出口等信息衔瓮。
每一個(gè)線程在創(chuàng)建的時(shí)候,jvm都會(huì)為其創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧抖甘,虛擬機(jī)棧的大小可以用-xss
來配置热鞍,方法的調(diào)用和結(jié)束就是棧幀壓入和彈出的過程,同等的虛擬機(jī)棧如果局部變量表等占用內(nèi)存越小被壓入的棧幀就越多衔彻,反之則壓入的棧幀越少薇宠,一般將棧幀的內(nèi)存大小稱為寬度,而棧幀的數(shù)量稱為虛擬機(jī)棧的深度艰额。
一個(gè)虛擬機(jī)棧對(duì)應(yīng)一個(gè)線程澄港,當(dāng)前CPU調(diào)度的那個(gè)線程叫活動(dòng)線程;一個(gè)棧幀對(duì)應(yīng)一個(gè)方法柄沮,或者線程的虛擬機(jī)棧的最頂部的棧幀代表了當(dāng)前正在執(zhí)行的方法回梧,而這個(gè)棧幀也稱“當(dāng)前棧幀”。
3.7.3 本地方法棧
JVM為本地方法所劃分的區(qū)域叫本地方法棧祖搓,這款區(qū)域內(nèi)存自由度高狱意,完全靠不同的JVM廠商來實(shí)現(xiàn),Java虛擬機(jī)規(guī)范并未給出明確的規(guī)定棕硫,但它一樣也是線程私有的內(nèi)存區(qū)域髓涯。
3.7.4 堆內(nèi)存
堆內(nèi)存是jvm中最大的一塊內(nèi)存區(qū)域,被所有的線程所共享哈扮,Java在運(yùn)行期間所創(chuàng)建的對(duì)象幾乎所有都存放在此區(qū)域纬纪,該區(qū)域也是垃圾回收期重點(diǎn)照顧的區(qū)域蚓再,因此堆也稱為“GC堆”。
堆內(nèi)存一般分為新生代和老年代包各。
3.7.5 方法區(qū)
方法區(qū)也是被多個(gè)線程所共享的內(nèi)存區(qū)域摘仅,它主要用于存放已經(jīng)被虛擬機(jī)加載的類信息、常量问畅、靜態(tài)變量娃属、即時(shí)編輯器(JIT)編譯后的代碼等數(shù)據(jù)。
雖然在Java虛擬機(jī)規(guī)范中护姆,方法區(qū)被劃為堆內(nèi)存的一個(gè)分區(qū)矾端,但是它還是經(jīng)常稱為“非堆”,有時(shí)候也稱為“持久代”
3.7.6 java8元空間
自JDK1.8后卵皂,JVM的內(nèi)存區(qū)域發(fā)生了一些改變秩铆,實(shí)際上是持久代內(nèi)存被徹底刪除,取而代之的元空間灯变。
元空間同樣是堆內(nèi)存的一部分殴玛,JVM為每個(gè)類加載器分配一塊內(nèi)存塊列表,進(jìn)行線性分配添祸,塊的大小取決于類加載器的類型.sun/反射滚粟、代理對(duì)應(yīng)的類加載器塊會(huì)分配的小一些,之前的版本會(huì)單獨(dú)卸載回收某個(gè)類刃泌,而現(xiàn)在是GC過程中發(fā)現(xiàn)某個(gè)類加載器已經(jīng)具備回收的條件凡壤,則會(huì)將整個(gè)類加載器相關(guān)的元空間全部回收掉,這樣可以減少內(nèi)存碎片蔬咬,節(jié)省GC掃描和壓縮的時(shí)間鲤遥。
3.7.7 Thread與虛擬機(jī)棧
JVM中,程序技術(shù)器是內(nèi)存較小的一塊林艘,而且該部分內(nèi)存不會(huì)出現(xiàn)任何溢出異常盖奈,與線程創(chuàng)建,運(yùn)行狐援,銷毀等關(guān)系的是虛擬機(jī)棧內(nèi)存钢坦,而且虛擬機(jī)棧內(nèi)存劃分的大小直接決定了一個(gè)JVM進(jìn)程中可以創(chuàng)建多少個(gè)線程。
我們可以粗略地認(rèn)為:一個(gè)Java進(jìn)程內(nèi)存的大猩督础:堆內(nèi)存+線程數(shù)*虛擬機(jī)棧內(nèi)存
4 守護(hù)線程
守護(hù)線程是一類比較特殊的線程爹凹,一般用于處理一些后臺(tái)的任務(wù),比如JDK的垃圾回收線程镶殷。
如果JVM中沒有一個(gè)非守護(hù)線程禾酱,則JVM的進(jìn)程會(huì)退出。
- 守護(hù)線程的設(shè)置非常簡(jiǎn)單:setDaemon(),true為守護(hù)線程,false為正常線程颤陶。
- 線程是否為守護(hù)線程和它的父線程有很大關(guān)系颗管,如果父線程是正常線程,則子線程也是正常線程滓走,反之亦然券腔,如果想修改可以用setDaemon()方法修改鲤竹。isDaemon()方法可以判斷該線程是不是守護(hù)線程谦炬。
- setDaemon只能在線程啟動(dòng)之前設(shè)置刺覆,否則會(huì)拋出IlleagalStateException.
守護(hù)線程通常用作執(zhí)行一些后天任務(wù),又是也稱它為“后臺(tái)線程”姨涡,當(dāng)你希望關(guān)閉某些線程的時(shí)候衩藤,或者退出JVM進(jìn)程的時(shí)候,一些線程能夠關(guān)閉涛漂,此時(shí)可以考慮用守護(hù)線程完成這樣的工作慷彤。
5 線程Sleep
sleep是一個(gè)靜態(tài)方法,一個(gè)需要傳入毫秒數(shù)怖喻,另一個(gè)需要傳入毫秒數(shù)和納秒數(shù)。
sleep會(huì)使當(dāng)前線程進(jìn)入指定毫秒數(shù)的休眠岁诉,暫停執(zhí)行锚沸,雖然給定了休眠時(shí)間,但是最終要以系統(tǒng)的定時(shí)器和調(diào)度器精度為準(zhǔn)涕癣。休眠有一個(gè)特性哗蜈,就是它不會(huì)放棄其monitor鎖的所有權(quán)。
建議使用TimeUnit代替Thread.sleep坠韩,TimeUnit為sleep提供了簡(jiǎn)潔的封裝距潘。
6 線程yield
yield方法是一種啟發(fā)式的方法,它會(huì)提醒調(diào)度器我愿意放棄當(dāng)前的CPU資源只搁,如果CPU資源不緊張音比,則會(huì)忽略此提醒。
使用yield方法通常會(huì)使線程從RUNNING狀態(tài)切換到RUNNABLE狀態(tài)氢惋,一般這個(gè)方法不太常用洞翩。
yield只是個(gè)提示,CPU調(diào)度器不能保證每次都能滿足yield提示焰望。
6.1 sleep與yield
yield方法實(shí)際上就是調(diào)用了sleep(0),但他們?cè)诒举|(zhì)上有很大差別骚亿。
- sleep會(huì)導(dǎo)致當(dāng)前線程暫停指定的時(shí)間,沒有CPU時(shí)間片的消耗熊赖。
- yield只是對(duì)CPU調(diào)度器的一個(gè)提示来屠,如果CPU沒有忽略這個(gè)提示,他會(huì)導(dǎo)致線程上下文的切換.
- sleep會(huì)導(dǎo)致線程短暫的block,會(huì)在給定的時(shí)間內(nèi)釋放CPU資源.
- yield會(huì)使處于RUNNING狀態(tài)的Thread進(jìn)入RUNNABLE狀態(tài).
- sleep幾乎能保證完成給定時(shí)間的休眠,而yield的提示不一定能保證.
- 一個(gè)線程sleep,另一個(gè)線程調(diào)用interrupt會(huì)捕獲到中斷信號(hào),而yield不會(huì).
7 線程優(yōu)先級(jí)
進(jìn)程有優(yōu)先級(jí),線程同樣也有優(yōu)先級(jí),理論上較高的優(yōu)先級(jí)會(huì)取得優(yōu)先被CPU調(diào)度的機(jī)會(huì),但是事實(shí)上也不是相當(dāng)如愿的,設(shè)置線程優(yōu)先級(jí)的操作也是一個(gè)hint操作
- 對(duì)于root用戶,他會(huì)hint操作系統(tǒng)想要你設(shè)置的優(yōu)先級(jí),否則它會(huì)忽略
- 如果CPU比較忙,設(shè)置優(yōu)先級(jí)可能會(huì)獲得更多的時(shí)間片,但是閑時(shí)優(yōu)先級(jí)的高低幾乎不起任何作用.
從設(shè)置線程優(yōu)先級(jí)的源碼可以知道,優(yōu)先級(jí)不能小于1,也不能大于10,如果指定的優(yōu)先級(jí)大于線程所在的線程組的優(yōu)先級(jí),那么會(huì)失效,取而代之的是group的最大優(yōu)先級(jí).
一般情況下,不會(huì)對(duì)線程設(shè)置優(yōu)先級(jí),更不會(huì)讓某些業(yè)務(wù)嚴(yán)重地以來線程的優(yōu)先級(jí),比如權(quán)重.
線程默認(rèn)的優(yōu)先級(jí)和它的父線程保持一致,一般情況下都是5,因?yàn)閙ain線程的優(yōu)先級(jí)為5,所有由它派生的線程的優(yōu)先級(jí)都為5.
8 獲取線程ID
public long getID()獲取線程的唯一ID,線程的ID在整個(gè)進(jìn)程中都是唯一的,并且是從0開始遞增.
9 獲取當(dāng)前線程
public static Thread currentThread()
用于返回當(dāng)前的執(zhí)行線程的引用,這個(gè)方法雖然很簡(jiǎn)單,但是使用非常廣泛.
10 設(shè)置線程上下文類加載器
public ClassLoader getContextClassLoader
獲取線程上下文的類加載器,就是說這個(gè)線程是由哪個(gè)類加載器加載的,如果沒有修改上下文類加載器,那么類加載器默認(rèn)就是保持與父線程一樣的類加載器
public void setContextClassLoader(ClassLoader cl)
這個(gè)方法可以打破Java類加載器的父委托機(jī)制,有時(shí)候該方法也稱為Java類加載器的后門.
11 線程Interrupt
如下方法都可以讓線程進(jìn)入阻塞狀態(tài),而使用interrupt就可以打斷阻塞.
一個(gè)線程通過上述方法進(jìn)入阻塞狀態(tài),若另一個(gè)線程調(diào)用了被阻塞線程的interrupt方法,則會(huì)打斷這種阻塞,因此這種方法稱為可中斷方法.
interrupt原理:在一個(gè)線程內(nèi)部存在著名為interrupt flag的標(biāo)識(shí),如果一個(gè)線程被interrupt,那么它的flag將被設(shè)置;如果當(dāng)前線程正在執(zhí)行可中斷方法被阻塞時(shí),調(diào)用interrupt使其中斷,反而會(huì)導(dǎo)致flag被清除(這個(gè)也不難理解,可中斷方法捕獲到了中斷信號(hào)后,為了不影響線程中其他方法的執(zhí)行,將線程的interrupt標(biāo)識(shí)重置也是一種設(shè)計(jì));如果線程已死,調(diào)用interrupt會(huì)被直接忽略.
11.1 isInterrupted()
isInterrupted()
方法主要判斷線程是否被中斷,該方法僅僅是對(duì)flag標(biāo)識(shí)的一個(gè)判斷,并不會(huì)影響標(biāo)識(shí)發(fā)生任何改變
11.2 interrupted()
interrupted()是一個(gè)靜態(tài)方法,雖然它可以判斷當(dāng)前線程是否被中斷,但是它和成員方法isInterrupted()有很大差別.調(diào)用該方法會(huì)直接擦除該線程的interrupt標(biāo)識(shí).
需要注意的是,如果當(dāng)前線程被打斷了,那么第一次調(diào)用interrupted()時(shí)會(huì)返回true并擦除interrupt標(biāo)識(shí),第二次包括以后的調(diào)用就會(huì)返回false,除非此期間線程有一次地被打斷.
isInterrupted()和interrupted()兩個(gè)方法實(shí)際調(diào)用的是
只是一個(gè)能擦除flag標(biāo)識(shí),一個(gè)沒有
12 線程join
join某個(gè)線程A,會(huì)使當(dāng)前線程B進(jìn)入等待,知道線程A結(jié)束生命周期,或者是到達(dá)指定時(shí)間,那么此段時(shí)間線程B是block的,而不是A線程.
13 如何關(guān)閉一個(gè)線程
13.1 正常關(guān)閉
- 線程結(jié)束生命周期結(jié)束
- 捕獲中斷信號(hào)關(guān)閉線程:
- 使用volatile開關(guān)控制:由于線程的interrupt標(biāo)識(shí)有可能會(huì)被擦除,或者邏輯單元中不會(huì)有任何可中斷方法,所以使用volatile修飾的flag標(biāo)識(shí)關(guān)閉線程也是一種常用做法.
14 異常退出
在一個(gè)線程的執(zhí)行單元中,是不允許拋出checked異常的,不論Thread中的run方法,還是Runnable中的run方法,如果線程在運(yùn)行過程中需要捕獲checked異常并且需要判斷是否還有運(yùn)行下去的必要,那么此時(shí)可以將checked異常封裝成unchecked異常(RuntimeException)拋出進(jìn)而結(jié)束線程的生命周期
15 進(jìn)程假死
所謂進(jìn)程假死,就是進(jìn)程雖然存在,但沒有日志輸出,程序不進(jìn)行任何的作業(yè),看起來像死了一樣,但事實(shí)上是沒有死的,程序之所以出現(xiàn)這樣的情況,大多是某個(gè)線程阻塞了,或者線程出現(xiàn)了死鎖的情況.