一驱入、進程與線程
進程是資源分配的最小單位,線程是cpu調(diào)度的最小單位氯析。線程也被稱為輕量級進程亏较。
所有與進程相關(guān)的資源,都被記錄在PCB中
進程是搶占處理及的調(diào)度單位掩缓;線程屬于某個進程雪情,共享其資源
一個 Java 程序的運行是 main 線程和多個其他線程同時運行。
二你辣、Thread中的start和run方法的區(qū)別
調(diào)用start()方法會創(chuàng)建一個新的子線程并啟動
run()方法只是Thread的一個普通方法的調(diào)用巡通,還是在主線程里執(zhí)行。
三舍哄、Thread和Runnable是什么關(guān)系宴凉?
Thread是實現(xiàn)了Runnable接口的類,是的run支持多線程表悬。
因java類的單一繼承原則弥锄,推薦多使用Runnable接口
四、如何給run()方法傳參蟆沫?
構(gòu)造函數(shù)傳參
成員變量傳參
回調(diào)函數(shù)傳參
五籽暇、如何實現(xiàn)處理線程的返回值?
實現(xiàn)的方式主要有三種:
主線程等待法
/*private Stringvalue;public void run() {? ? try {? ? ? ? Thread.currentThread().sleep(5000);? ? } catch (InterruptedException e) {? ? ? ? e.printStackTrace();? ? }? ? value ="we have data now";}*/CycleWait cw = new CycleWait();Thread t = new Thread(cw);t.start();while(cw.value== null){? ? ? ? ? Thread.currentThread().sleep(100);//如果value一直為空饭庞,則線程一直sleep? ? ? ? }
使用Thread類的join()阻塞當前線程戒悠,以等待子線程處理完畢
t.join();
通過Callable接口實現(xiàn):通過FutureTask Or 線程池獲取
六、線程的狀態(tài)但绕?
新建(NEW)?:創(chuàng)建后尚未啟動的線程的狀態(tài)
運行(Runnable)?:包含Running和Ready
無限期等待(Waiting)?:不會被分配CPU執(zhí)行時間救崔,需要顯式被喚醒
沒有設(shè)置Timeout參數(shù)的Object.wait()方法惶看。
沒有設(shè)置Timeout參數(shù)的Thread.join()方法。
LockSupport.park()方法六孵。
限期等待(Timed Waiting)?:在一定時間后會由系統(tǒng)自動喚醒
Thread.sleep()方法纬黎。
設(shè)置了Timeout參數(shù)的Object.wait()方法。
設(shè)置了Timeout參數(shù)的Thread.join()方法劫窒。
LockSupport.parkNanos()方法本今。
LockSupport.parkUntil()方法。
阻塞(blocked)?:等待獲取排它鎖
結(jié)束?:已終止線程的狀態(tài)主巍,線程已經(jīng)結(jié)束執(zhí)行
七冠息、sleep和wait
sleep是Thread類的方法,wait是Object類中定義的方法
sleep方法可以在任何地方使用
wait方法只能在synchronized方法或者synchronized塊中使用
最本質(zhì)的區(qū)別
Thread.sleep只會讓出CPU孕索,不會導致鎖行為的改變(不會釋放鎖)
Object.wait不僅讓出CPU逛艰,還會釋放已經(jīng)占有的同步資源鎖
八、notify和notifyAll的區(qū)別
notifyAll會讓所有處于等待池的線程全部進入鎖池去競爭獲取鎖的機會
notify會隨機選取一個處于等待池中的線程進入鎖池去競爭獲取鎖的機會搞旭。
九散怖、yield函數(shù)
當調(diào)用Thread.yield()函數(shù)時,會給線程調(diào)度器一個當前線程愿意讓出CPU使用的暗示肄渗,但是線程調(diào)度器可能會忽略這個暗示
十镇眷、中斷函數(shù)interrupt()
已經(jīng)被拋棄的方法
通過調(diào)用stop()方法停止線程
目前使用的方法
調(diào)用interrupt(),通知線程應(yīng)該中斷了
如果線程處于被阻塞狀態(tài)翎嫡,那么線程將立即退出被阻塞狀態(tài)欠动,并拋出一個InterruptedException異常
如果線程處于正常活動狀態(tài)惑申,那么會將該線程的中斷標志設(shè)置為true具伍。被設(shè)置中斷標志的線程將繼續(xù)正常運行,不受影響
需要被調(diào)用的線程配合中斷
在正常運行任務(wù)時圈驼,經(jīng)常檢查本線程的中斷標志位沿猜,如果被設(shè)置了中斷標志就自行停止線程。
如果線程處于正惩爰梗活動狀態(tài),那么會將該線程的中斷標志設(shè)置為true橄妆。被設(shè)置中斷標志的線程將繼續(xù)正常運行衙伶,不受影響
十一、synchronized
線程安全問題的主要誘因:
存在共享數(shù)據(jù)(也稱臨界資源)
存在多條線程共同操作這些共享數(shù)據(jù)
解決問題的根本辦法:同一時刻有且只有一個線程在操作共享數(shù)據(jù)害碾,其他線程必須等到該線程處理完數(shù)據(jù)后再對貢獻數(shù)據(jù)進行操作矢劲。
互斥鎖的特性:
互斥性?:即在同一時間只允許一個線程持有某個對象鎖,通過這種特性來實現(xiàn)多線程的協(xié)調(diào)機制慌随,這樣同一時間只有一個線程對需要同步的代碼塊(復合操作)進行訪問芬沉√赏互斥性也稱為操作的原子性。
可見性?:必須確保在鎖被釋放之前丸逸,對共享變量所做的修改蹋艺,對于隨后獲得該鎖的另一個線程是可見的(即在獲得鎖時應(yīng)該獲得最新共享變量的值),否則另一個線程可能是在本地緩存的某個副本上繼續(xù)操作黄刚,從而引起不一致捎谨。(一致性?憔维?涛救?paxos?业扒?检吆?raft?程储?蹭沛?)
根據(jù)獲取鎖的分類:獲取對象鎖和獲取類鎖
獲取對象鎖的兩種用法
同步代碼塊(synchronized(this),synchronized(類實例對象))虱肄,鎖是小括號()中的實例對象致板。
同步非靜態(tài)方法(synchronized method),鎖是當前對象的實例對象咏窿。
獲取類鎖的兩種用法
同步代碼塊(synchronized(類.class))斟或,鎖是小括號()中的類對象(Class對象)。
同步靜態(tài)方法(synchronized static method),鎖是當前對象的類對象(Class對象)
對象鎖和類鎖的總結(jié):
有線程訪問對象的同步代碼塊時集嵌,另外的線程可以訪問該對象的非同步代碼塊萝挤;
若鎖住的是同一個對象,一個線程在訪問對象的同步代碼塊時根欧,另一個訪問對象的同步代碼塊的線程會被阻塞怜珍;
若鎖住的是同一個對象,一個線程在訪問對象的同步方法時凤粗,另一個訪問對象的同步方法的線程會被阻塞酥泛;
若鎖住的是同一個對象,一個線程在訪問對象的同步代碼塊時嫌拣,另一個訪問對象的同步方法的線程會被阻塞柔袁;,反之亦然异逐;
同一個類的不同對象的對象鎖互不干擾捶索;
類鎖由于也是一種特殊的對象鎖,因此表現(xiàn)和上述1灰瞻、2、3、4一致洗做,而由于一個類只有一把對象鎖,所以同一個類的不同對象使用類鎖將會是同步的璃弄;
類鎖和對象鎖互不干擾。
十二底瓣、synchronized的底層實現(xiàn)原理
1. 實現(xiàn)synchronized的基礎(chǔ)
java對象頭
Monitor
2. 對象在內(nèi)存中的布局
對象頭
實例數(shù)據(jù)
對齊填充
對象頭的結(jié)構(gòu):
java的對象頭由以下三部分組成:
Mark Word
指向類的指針
數(shù)組長度(只有數(shù)組對象才有)
Mark Word Mark Word記錄了對象和鎖有關(guān)的信息谢揪,當這個對象被synchronized關(guān)鍵字當成同步鎖時,圍繞這個鎖的一系列操作都和Mark Word有關(guān)捐凭。
Mark Word在32位JVM中的長度是32bit拨扶,在64位JVM中長度是64bit。
Mark Word在不同的鎖狀態(tài)下存儲的內(nèi)容不同茁肠,在32位JVM中是這么存的:
JVM一般是這樣使用鎖和Mark Word的:
當沒有被當成鎖時患民,這就是一個普通的對象,Mark Word記錄對象的HashCode垦梆,鎖標志位是01匹颤,是否偏向鎖那一位是0。
當對象被當做同步鎖并有一個線程A搶到了鎖時托猩,鎖標志位還是01印蓖,但是否偏向鎖那一位改成1,前23bit記錄搶到鎖的線程id京腥,表示進入偏向鎖狀態(tài)赦肃。
當線程A再次試圖來獲得鎖時,JVM發(fā)現(xiàn)同步鎖對象的標志位是01公浪,是否偏向鎖是1他宛,也就是偏向狀態(tài),Mark Word中記錄的線程id就是線程A自己的id欠气,表示線程A已經(jīng)獲得了這個偏向鎖厅各,可以執(zhí)行同步鎖的代碼。
當線程B試圖獲得這個鎖時预柒,JVM發(fā)現(xiàn)同步鎖處于偏向狀態(tài)队塘,但是Mark Word中的線程id記錄的不是B,那么線程B會先用CAS操作試圖獲得鎖宜鸯,這里的獲得鎖操作是有可能成功的人灼,因為線程A一般不會自動釋放偏向鎖。如果搶鎖成功顾翼,就把Mark Word里的線程id改為線程B的id,代表線程B獲得了這個偏向鎖奈泪,可以執(zhí)行同步鎖代碼适贸。如果搶鎖失敗灸芳,則繼續(xù)執(zhí)行步驟5。
偏向鎖狀態(tài)搶鎖失敗拜姿,代表當前鎖有一定的競爭烙样,偏向鎖將升級為輕量級鎖。JVM會在當前線程的線程棧中開辟一塊單獨的空間蕊肥,里面保存指向?qū)ο箧iMark Word的指針谒获,同時在對象鎖Mark Word中保存指向這片空間的指針。上述兩個保存操作都是CAS操作壁却,如果保存成功批狱,代表線程搶到了同步鎖,就把Mark Word中的鎖標志位改成00展东,可以執(zhí)行同步鎖代碼赔硫。如果保存失敗,表示搶鎖失敗盐肃,競爭太激烈爪膊,繼續(xù)執(zhí)行步驟6。
輕量級鎖搶鎖失敗砸王,JVM會使用自旋鎖推盛,自旋鎖不是一個鎖狀態(tài),只是代表不斷的重試谦铃,嘗試搶鎖耘成。從JDK1.7開始,自旋鎖默認啟用荷辕,自旋次數(shù)由JVM決定凿跳。如果搶鎖成功則執(zhí)行同步鎖代碼,如果失敗則繼續(xù)執(zhí)行步驟7疮方。
自旋鎖重試之后如果搶鎖依然失敗控嗜,同步鎖會升級至重量級鎖,鎖標志位改為10骡显。在這個狀態(tài)下疆栏,未搶到鎖的線程都會被阻塞。
Monitor(管程):每個java對象天生自帶了一把看不見的鎖
Monitor鎖的競爭惫谤、獲取與釋放
十三壁顶、自旋鎖
許多情況下,共享數(shù)據(jù)的所狀態(tài)持續(xù)時間較短溜歪,切換線程不值得若专。
通過讓線程執(zhí)行忙循環(huán)等待鎖的釋放,不讓出cpu蝴猪。
缺點:若鎖被其他線程長時間占用调衰,會帶來許多性能上的開銷·
十四膊爪、自適應(yīng)自旋鎖
自旋的次數(shù)不再固定
由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定
十五、鎖消除
JIT編譯時嚎莉,對運行上下文進行掃描米酬,去除不可能存在競爭的鎖。
十六趋箩、鎖粗化
通過擴大鎖的范圍赃额,避免反復的加鎖解鎖
十七、synchronized的四種狀態(tài)
無鎖叫确、偏向鎖跳芳、輕量級鎖、重量級鎖
鎖膨脹方向:無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
偏向鎖:減少同一線程獲取鎖的代價
大多數(shù)情況下启妹,鎖不存在多線程競爭筛严,總是由同一線程多次獲得
核心思想:
如果一個線程獲得了鎖,那么鎖就進入了偏向模式饶米,此時Mark Word的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu)桨啃,當該線程再次請求鎖時,無需再做任何同步操作檬输,即獲取鎖的過程只需要檢查Mark Word的所標記位為偏向鎖以及當前線程ID等于Mark Word的ThreadID即可照瘾,這樣就省去了大量有關(guān)鎖申請的操作。
十八丧慈、輕量級鎖
輕量級鎖是由偏向鎖升級來的析命,偏向鎖運行在一個線程進入同步塊的情況下,當?shù)诙€線程加入鎖爭用的時候逃默,偏向鎖就會升級為輕量級鎖鹃愤。
?適用場景:?線程交替執(zhí)行同步塊
若存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖
十九完域、鎖的內(nèi)存語義
當線程釋放鎖時软吐,JMM會把該線程對應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。
當線程獲取鎖時吟税,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效凹耙。從而使得被監(jiān)視器保護的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量。
二十肠仪、ReenTrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖肖抱。
publicclassLockExample{privateLocklock=newReentrantLock();publicvoidfunc(){lock.lock();try{for(inti =0; i <10; i++) {? ? ? ? ? ? ? ? System.out.print(i +" ");? ? ? ? ? ? }? ? ? ? }finally{lock.unlock();// 確保釋放鎖,從而避免發(fā)生死鎖异旧。}? ? }}
public staticvoidmain(String[] args) {? ? LockExample lockExample =newLockExample();? ? ExecutorService executorService = Executors.newCachedThreadPool();? ? executorService.execute(() -> lockExample.func());executorService.execute(() -> lockExample.func());}
1. 鎖的實現(xiàn)
synchronized 是 JVM 實現(xiàn)的意述,而 ReentrantLock 是 JDK 實現(xiàn)的。
2. 性能
新版本 Java 對 synchronized 進行了很多優(yōu)化,例如自旋鎖等荤崇,synchronized 與 ReentrantLock 大致相同镐依。
3. 等待可中斷
當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待天试,改為處理其他事情。
ReentrantLock 可中斷然低,而 synchronized 不行喜每。
4. 公平鎖
公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖雳攘。
synchronized 中的鎖是非公平的带兜,ReentrantLock 默認情況下也是非公平的,但是也可以是公平的吨灭。
5. 鎖綁定多個條件
一個 ReentrantLock 可以同時綁定多個 Condition 對象刚照。
二十一、線程池
1. 為什么要用線程池喧兄?
線程池提供了一種限制和管理資源(包括執(zhí)行一個任務(wù))无畔。 每個線程池還維護一些基本統(tǒng)計信息,例如已完成任務(wù)的數(shù)量吠冤。
這里借用《Java并發(fā)編程的藝術(shù)》提到的來說一下使用線程池的好處:
降低資源消耗?浑彰。 通過重復利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
提高響應(yīng)速度?拯辙。 當任務(wù)到達時郭变,任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行。
提高線程的可管理性?涯保。 線程是稀缺資源诉濒,如果無限制的創(chuàng)建,不僅會消耗系統(tǒng)資源夕春,還會降低系統(tǒng)的穩(wěn)定性未荒,使用線程池可以進行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控撇他。
2. 實現(xiàn)Runnable接口和Callable接口的區(qū)別
如果想讓線程池執(zhí)行任務(wù)的話需要實現(xiàn)的Runnable接口或Callable接口茄猫。 Runnable接口或Callable接口實現(xiàn)類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執(zhí)行。兩者的區(qū)別在于 Runnable 接口不會返回結(jié)果但是 Callable 接口可以返回結(jié)果困肩。
備注: 工具類Executors可以實現(xiàn)Runnable對象和Callable對象之間的相互轉(zhuǎn)換划纽。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))锌畸。
3. 執(zhí)行execute()方法和submit()方法的區(qū)別是什么呢勇劣?
execute() 方法用于提交不需要返回值的任務(wù),所以無法判斷任務(wù)是否被線程池執(zhí)行成功與否;
submit() 方法用于提交需要返回值的任務(wù)比默。線程池會返回一個Future類型的對象幻捏,通過這個Future對象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過future的get()方法來獲取返回值命咐,get()方法會阻塞當前線程直到任務(wù)完成篡九,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間后立即返回醋奠,這時候有可能任務(wù)沒有執(zhí)行完榛臼。
4. 如何創(chuàng)建線程池
《阿里巴巴Java開發(fā)手冊》中強制線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式窜司,這樣的處理方式讓寫的同學更加明確線程池的運行規(guī)則沛善,規(guī)避資源耗盡的風險。
Executors 返回線程池對象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor?: 允許請求的隊列長度為 Integer.MAX_VALUE 塞祈,可能堆積大量的請求金刁,從而導致OOM。
CachedThreadPool 和 ScheduledThreadPool?: 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE 议薪,可能會創(chuàng)建大量線程尤蛮,從而導致OOM。
方式一:通過Executor 框架的工具類Executors來實現(xiàn) 我們可以創(chuàng)建三種類型的ThreadPoolExecutor:
FixedThreadPool?: 該方法返回一個固定線程數(shù)量的線程池笙蒙。該線程池中的線程數(shù)量始終不變抵屿。當有一個新的任務(wù)提交時,線程池中若有空閑線程捅位,則立即執(zhí)行轧葛。若沒有,則新的任務(wù)會被暫存在一個任務(wù)隊列中艇搀,待有線程空閑時尿扯,便處理在任務(wù)隊列中的任務(wù)。
SingleThreadExecutor?: 方法返回一個只有一個線程的線程池焰雕。若多余一個任務(wù)被提交到該線程池衷笋,任務(wù)會被保存在一個任務(wù)隊列中,待線程空閑矩屁,按先入先出的順序執(zhí)行隊列中的任務(wù)辟宗。
CachedThreadPool?: 該方法返回一個可根據(jù)實際情況調(diào)整線程數(shù)量的線程池。線程池的線程數(shù)量不確定吝秕,但若有空閑線程可以復用泊脐,則會優(yōu)先使用可復用的線程。若所有線程均在工作烁峭,又有新的任務(wù)提交容客,則會創(chuàng)建新的線程處理任務(wù)秕铛。所有線程在當前任務(wù)執(zhí)行完畢后,將返回線程池進行復用缩挑。
二十二但两、volatile關(guān)鍵字
在 JDK1.2 之前,Java的內(nèi)存模型實現(xiàn)總是從主存(即共享內(nèi)存)讀取變量供置,是不需要進行特別的注意的谨湘。而在當前的 Java 內(nèi)存模型下,線程可以把變量保存本地內(nèi)存比如機器的寄存器)中芥丧,而不是直接在主存中進行讀寫悲关。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續(xù)使用它在寄存器中的變量值的拷貝娄柳,造成數(shù)據(jù)的不一致。
要解決這個問題艘绍,就需要把變量聲明為volatile赤拒,這就指示 JVM,這個變量是不穩(wěn)定的诱鞠,每次使用它都到主存中進行讀取挎挖。
說白了, volatile 關(guān)鍵字的主要作用就是保證變量的可見性然后還有一個作用是防止指令重排序航夺。
二十三蕉朵、synchronized 關(guān)鍵字和 volatile 關(guān)鍵字的區(qū)別
synchronized關(guān)鍵字和volatile關(guān)鍵字比較
volatile關(guān)鍵字是線程同步的輕量級實現(xiàn),所以volatile性能肯定比synchronized關(guān)鍵字要好阳掐。但是volatile關(guān)鍵字只能用于變量而synchronized關(guān)鍵字可以修飾方法以及代碼塊始衅。synchronized關(guān)鍵字在JavaSE1.6之后進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優(yōu)化之后執(zhí)行效率有了顯著提升,實際開發(fā)中使用 synchronized 關(guān)鍵字的場景還是更多一些缭保。
多線程訪問volatile關(guān)鍵字不會發(fā)生阻塞汛闸,而synchronized關(guān)鍵字可能會發(fā)生阻塞
volatile關(guān)鍵字能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性艺骂。synchronized關(guān)鍵字兩者都能保證诸老。
volatile關(guān)鍵字主要用于解決變量在多個線程之間的可見性,而 synchronized關(guān)鍵字解決的是多個線程之間訪問資源的同步性钳恕。
二十四别伏、ThreadLocal
通常情況下,我們創(chuàng)建的變量是可以被任何一個線程訪問并修改的忧额。如果想實現(xiàn)每一個線程都有自己的專屬本地變量該如何解決呢厘肮? JDK中提供的ThreadLocal類正是為了解決這樣的問題。 ThreadLocal類主要解決的就是讓每個線程綁定自己的值宙址,可以將ThreadLocal類形象的比喻成存放數(shù)據(jù)的盒子轴脐,盒子中可以存儲每個線程的私有數(shù)據(jù)。
如果你創(chuàng)建了一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的本地副本大咱,這也是ThreadLocal變量名的由來恬涧。他們可以使用 get() 和 set() 方法來獲取默認值或?qū)⑵渲蹈臑楫斍熬€程所存的副本的值,從而避免了線程安全問題碴巾。
importjava.text.SimpleDateFormat;importjava.util.Random;publicclassThreadLocalExampleimplementsRunnable{// SimpleDateFormat 不是線程安全的溯捆,所以每個線程都要有自己獨立的副本privatestaticfinalThreadLocal formatter = ThreadLocal.withInitial(() ->newSimpleDateFormat("yyyyMMdd HHmm"));publicstaticvoidmain(String[] args)throwsInterruptedException{? ? ? ? ThreadLocalExample obj =newThreadLocalExample();for(inti=0; i<10; i++){? ? ? ? ? ? Thread t =newThread(obj,""+i);? ? ? ? ? ? Thread.sleep(newRandom().nextInt(1000));? ? ? ? ? ? t.start();? ? ? ? }? ? }@Overridepublicvoidrun(){? ? ? ? System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());try{? ? ? ? ? ? Thread.sleep(newRandom().nextInt(1000));? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }//formatter pattern is changed here by thread, but it won't reflect to other threadsformatter.set(newSimpleDateFormat());? ? ? ? System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());? ? }}
Output:
ThreadName=0defaultFormatter = yyyyMMdd HHmmThreadName=0formatter = yy-M-d ah:mmThreadName=1defaultFormatter = yyyyMMdd HHmmThreadName=2defaultFormatter = yyyyMMdd HHmmThreadName=1formatter = yy-M-d ah:mmThreadName=3defaultFormatter = yyyyMMdd HHmmThreadName=2formatter = yy-M-d ah:mmThreadName=4defaultFormatter = yyyyMMdd HHmmThreadName=3formatter = yy-M-d ah:mmThreadName=4formatter = yy-M-d ah:mmThreadName=5defaultFormatter = yyyyMMdd HHmmThreadName=5formatter = yy-M-d ah:mmThreadName=6defaultFormatter = yyyyMMdd HHmmThreadName=6formatter = yy-M-d ah:mmThreadName=7defaultFormatter = yyyyMMdd HHmmThreadName=7formatter = yy-M-d ah:mmThreadName=8defaultFormatter = yyyyMMdd HHmmThreadName=9defaultFormatter = yyyyMMdd HHmmThreadName=8formatter = yy-M-d ah:mmThreadName=9formatter = yy-M-d ah:mm
原理:
從 Thread類源代碼入手。
publicclassThreadimplementsRunnable{ ......//與此線程有關(guān)的ThreadLocal值厦瓢。由ThreadLocal類維護ThreadLocal.ThreadLocalMap threadLocals =null;//與此線程有關(guān)的InheritableThreadLocal值提揍。由InheritableThreadLocal類維護ThreadLocal.ThreadLocalMap inheritableThreadLocals =null; ......}
從上面Thread類 源代碼可以看出Thread 類中有一個 threadLocals 和 一個 inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類型的變量,我們可以把 ThreadLocalMap 理解為ThreadLocal 類實現(xiàn)的定制化的 HashMap煮仇。默認情況下這兩個變量都是null劳跃,只有當前線程調(diào)用 ThreadLocal 類的 set或get方法時才創(chuàng)建它們,實際上調(diào)用這兩個方法的時候浙垫,我們調(diào)用的是ThreadLocalMap類對應(yīng)的 get()刨仑、set() 方法。
ThreadLocal類的set()方法
publicvoidset(Tvalue){? ? ? ? Thread t = Thread.currentThread();? ? ? ? ThreadLocalMap map = getMap(t);if(map !=null)? ? ? ? ? ? map.set(this,value);elsecreateMap(t,value);? ? }ThreadLocalMapgetMap(Thread t){returnt.threadLocals;? ? }
通過上面這些內(nèi)容夹姥,我們足以通過猜測得出結(jié)論:最終的變量是放在了當前線程的 ThreadLocalMap 中杉武,并不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝辙售,傳遞了變量值轻抱。 ThrealLocal 類中可以通過Thread.currentThread()獲取到當前線程對象后,直接通過getMap(Thread t)可以訪問到該線程的ThreadLocalMap對象旦部。
每個Thread中都具備一個ThreadLocalMap祈搜,而ThreadLocalMap可以存儲以ThreadLocal為key的鍵值對。 比如我們在同一個線程中聲明了兩個 ThreadLocal 對象的話士八,會使用 Thread內(nèi)部都是使用僅有那個ThreadLocalMap 存放數(shù)據(jù)的夭问,ThreadLocalMap的 key 就是 ThreadLocal對象,value 就是 ThreadLocal 對象調(diào)用set方法設(shè)置的值曹铃。 ThreadLocal 是 map結(jié)構(gòu)是為了讓每個線程可以關(guān)聯(lián)多個 ThreadLocal變量缰趋。這也就解釋了 ThreadLocal 聲明的變量為什么在每一個線程都有自己的專屬本地變量。
ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類陕见。
二十五秘血、ThreadLocal 內(nèi)存泄露問題
ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,而 value 是強引用。所以评甜,如果 ThreadLocal 沒有被外部強引用的情況下灰粮,在垃圾回收的時候會 key 會被清理掉,而 value 不會被清理掉忍坷。這樣一來粘舟,ThreadLocalMap 中就會出現(xiàn)key為null的Entry熔脂。假如我們不做任何措施的話,value 永遠無法被GC 回收柑肴,這個時候就可能會產(chǎn)生內(nèi)存泄露霞揉。ThreadLocalMap實現(xiàn)中已經(jīng)考慮了這種情況,在調(diào)用 set()晰骑、get()适秩、remove() 方法的時候,會清理掉 key 為 null 的記錄硕舆。使用完 ThreadLocal方法后 最好手動調(diào)用remove()方法秽荞。
staticclassEntryextendsWeakReference<ThreadLocal<?>>{/** The value associated with this ThreadLocal. */Objectvalue;Entry(ThreadLocal k,Objectv) {super(k);? ? ? ? ? ? ? ? value = v;? ? ? ? ? ? }? ? ? ? }
弱引用介紹:
如果一個對象只具有弱引用,那么就類似于可有可無的生活用品抚官。弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期扬跋。在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象凌节,不管當前內(nèi)存空間足夠與否胁住,都會回收它的內(nèi)存。不過刊咳,由于垃圾回收器是一個優(yōu)先級很低的線程, 因此不一定會很快發(fā)現(xiàn)那些只具有弱引用的對象儡司。
弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用娱挨,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關(guān)聯(lián)的引用隊列中去捕犬。
二十六跷坝、java 線程方法join的簡單總結(jié)
1. 作用
Thread類中的join方法的主要作用就是同步,它可以使得線程之間的并行執(zhí)行變?yōu)榇袌?zhí)行碉碉。具體看代碼:
publicclassJoinTest{publicstaticvoidmain(String [] args)throwsInterruptedException{? ? ? ? ThreadJoinTest t1 =newThreadJoinTest("小明");? ? ? ? ThreadJoinTest t2 =newThreadJoinTest("小東");? ? ? ? t1.start();/**join的意思是使得放棄當前線程的執(zhí)行柴钻,并返回對應(yīng)的線程,例如下面代碼的意思就是:
? ? ? ? 程序在main線程中調(diào)用t1線程的join方法贴届,則main線程放棄cpu控制權(quán)蜡吧,并返回t1線程繼續(xù)執(zhí)行直到線程t1執(zhí)行完畢
? ? ? ? 所以結(jié)果是t1線程執(zhí)行完后,才到主線程執(zhí)行昔善,相當于在main線程中同步t1線程元潘,t1執(zhí)行完了君仆,main線程才有執(zhí)行的機會
? ? ? ? */t1.join();? ? ? ? t2.start();? ? }}classThreadJoinTestextendsThread{publicThreadJoinTest(String name){super(name);? ? }@Overridepublicvoidrun(){for(inti=0;i<1000;i++){? ? ? ? ? ? System.out.println(this.getName() +":"+ i);? ? ? ? }? ? }}
上面程序結(jié)果是先打印完小明線程牲距,在打印小東線程;
上面注釋也大概說明了join方法的作用:在A線程中調(diào)用了B線程的join()方法時钥庇,表示只有當B線程執(zhí)行完畢時牍鞠,A線程才能繼續(xù)執(zhí)行皮服。注意参咙,這里調(diào)用的join方法是沒有傳參的,join方法其實也可以傳遞一個參數(shù)給它的择同,具體看下面的簡單例子:
publicclassJoinTest{publicstaticvoidmain(String [] args)throwsInterruptedException{? ? ? ? ThreadJoinTest t1 =newThreadJoinTest("小明");? ? ? ? ThreadJoinTest t2 =newThreadJoinTest("小東");? ? ? ? t1.start();/**join方法可以傳遞參數(shù)净宵,join(10)表示main線程會等待t1線程10毫秒,10毫秒過去后紧武,
? ? ? ? * main線程和t1線程之間執(zhí)行順序由串行執(zhí)行變?yōu)槠胀ǖ牟⑿袌?zhí)行
? ? ? ? */t1.join(10);? ? ? ? t2.start();? ? }}classThreadJoinTestextendsThread{publicThreadJoinTest(String name){super(name);? ? }@Overridepublicvoidrun(){for(inti=0;i<1000;i++){? ? ? ? ? ? System.out.println(this.getName() +":"+ i);? ? ? ? }? ? }}
上面代碼結(jié)果是:程序執(zhí)行前面10毫秒內(nèi)打印的都是小明線程敏储,10毫秒后,小明和小東程序交替打印妥箕。
所以更舞,join方法中如果傳入?yún)?shù)缆蝉,則表示這樣的意思:如果A線程中掉用B線程的join(10),則表示A線程會等待B線程執(zhí)行10毫秒贝搁,10毫秒過后芽偏,A、B線程并行執(zhí)行膀哲。需要注意的是,jdk規(guī)定某宪,join(0)的意思不是A線程等待B線程0秒兴喂,而是A線程等待B線程無限時間,直到B線程執(zhí)行完畢畏鼓,即join(0)等價于join()壶谒。
2. join與start調(diào)用順序問題
上面的討論大概知道了join的作用了云矫,那么让禀,如果 join在start前調(diào)用陨界,會出現(xiàn)什么后果呢?先看下面的測試結(jié)果
publicclassJoinTest{publicstaticvoidmain(String [] args)throwsInterruptedException{? ? ? ? ThreadJoinTest t1 =newThreadJoinTest("小明");? ? ? ? ThreadJoinTest t2 =newThreadJoinTest("小東");/**join方法可以在start方法前調(diào)用時腮敌,并不能起到同步的作用
? ? ? ? */t1.join();? ? ? ? t1.start();//Thread.yield();t2.start();? ? }}classThreadJoinTestextendsThread{publicThreadJoinTest(String name){super(name);? ? }@Overridepublicvoidrun(){for(inti=0;i<1000;i++){? ? ? ? ? ? System.out.println(this.getName() +":"+ i);? ? ? ? }? ? }}
上面代碼執(zhí)行結(jié)果是:小明和小東線程交替打印麻车。
所以得到以下結(jié)論:join方法必須在線程start方法調(diào)用之后調(diào)用才有意義动猬。這個也很容易理解:如果一個線程都沒有start表箭,那它也就無法同步了。
3. join方法實現(xiàn)原理
有了上面的例子彼水,我們大概知道join方法的作用了极舔,那么拆魏,join方法實現(xiàn)的原理是什么呢慈俯?
其實拥峦,join方法是通過調(diào)用線程的wait方法來達到同步的目的的。例如刑峡,A線程中調(diào)用了B線程的join方法玄柠,則相當于A線程調(diào)用了B線程的wait方法随闪,在調(diào)用了B線程的wait方法后,A線程就會進入阻塞狀態(tài)撮奏,具體看下面的源碼:
publicfinalsynchronizedvoidjoin(longmillis)throwsInterruptedException{longbase = System.currentTimeMillis();longnow =0;if(millis <0) {thrownewIllegalArgumentException("timeout value is negative");? ? ? ? }if(millis ==0) {while(isAlive()) {? ? ? ? ? ? ? ? wait(0);? ? ? ? ? ? }? ? ? ? }else{while(isAlive()) {longdelay = millis - now;if(delay <=0) {break;? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? wait(delay);? ? ? ? ? ? ? ? now = System.currentTimeMillis() - base;? ? ? ? ? ? }? ? ? ? }? ? }
從源碼中可以看到:join方法的原理就是調(diào)用相應(yīng)線程的wait方法進行等待操作的当宴,例如A線程中調(diào)用了B線程的join方法户矢,則相當于在A線程中調(diào)用了B線程的wait方法,當B線程執(zhí)行完(或者到達等待時間)捌年,B線程會自動調(diào)用自身的notifyAll方法喚醒A線程挂洛,從而達到同步的目的虏劲。
二十七、線程安全
多個線程不管以何種方式訪問某個類励堡,并且在主調(diào)代碼中不需要進行同步,都能表現(xiàn)正確的行為泉唁。
線程安全有以下幾種實現(xiàn)方式:
1. 不可變
不可變(Immutable)的對象一定是線程安全的,不需要再采取任何的線程安全保障措施漩绵。只要一個不可變的對象被正確地構(gòu)建出來肛炮,永遠也不會看到它在多個線程之中處于不一致的狀態(tài)。多線程環(huán)境下碍扔,應(yīng)當盡量使對象成為不可變不同,來滿足線程安全溶耘。
不可變的類型:
final 關(guān)鍵字修飾的基本數(shù)據(jù)類型
String
枚舉類型
Number 部分子類凳兵,如 Long 和 Double 等數(shù)值包裝類型,BigInteger 和 BigDecimal 等大數(shù)據(jù)類型饭望。但同為 Number 的原子類 AtomicInteger 和 AtomicLong 則是可變的形庭。
對于集合類型萨醒,可以使用 Collections.unmodifiableXXX() 方法來獲取一個不可變的集合。
publicclassImmutableExample{? ? publicstaticvoidmain(String[] args) {Map map =newHashMap<>();Map unmodifiableMap = Collections.unmodifiableMap(map);? ? ? ? unmodifiableMap.put("a",1);? ? }}Exceptioninthread"main"java.lang.UnsupportedOperationException? ? at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)? ? at ImmutableExample.main(ImmutableExample.java:9)
Collections.unmodifiableXXX() 先對原始的集合進行拷貝倍宾,需要對集合進行修改的方法都直接拋出異常胜嗓。
publicVput(K key, Vvalue){thrownewUnsupportedOperationException();}
2. 互斥同步
synchronized 和 ReentrantLock辞州。
3. 非阻塞同步
互斥同步最主要的問題就是線程阻塞和喚醒所帶來的性能問題寥粹,因此這種同步也稱為阻塞同步。
互斥同步屬于一種悲觀的并發(fā)策略媚狰,總是認為只要不去做正確的同步措施崭孤,那就肯定會出現(xiàn)問題。無論共享數(shù)據(jù)是否真的會出現(xiàn)競爭遗锣,它都要進行加鎖(這里討論的是概念模型精偿,實際上虛擬機會優(yōu)化掉很大一部分不必要的加鎖)赋兵、用戶態(tài)核心態(tài)轉(zhuǎn)換、維護鎖計數(shù)器和檢查是否有被阻塞的線程需要喚醒等操作拓轻。
①. CAS
隨著硬件指令集的發(fā)展扶叉,我們可以使用基于沖突檢測的樂觀并發(fā)策略:先進行操作帕膜,如果沒有其它線程爭用共享數(shù)據(jù)垮刹,那操作就成功了,否則采取補償措施(不斷地重試酪劫,直到成功為止)寺董。這種樂觀的并發(fā)策略的許多實現(xiàn)都不需要將線程阻塞遮咖,因此這種同步操作稱為非阻塞同步。
樂觀鎖需要操作和沖突檢測這兩個步驟具備原子性麦箍,這里就不能再使用互斥同步來保證了,只能靠硬件來完成享钞。硬件支持的原子性操作最典型的是:比較并交換(Compare-and-Swap嫩与,CAS)交排。CAS 指令需要有 3 個操作數(shù)埃篓,分別是內(nèi)存地址 V、舊的預(yù)期值 A 和新值 B同窘。當執(zhí)行操作時部脚,只有當 V 的值等于 A委刘,才將 V 的值更新為 B。
②. AtomicInteger
J.U.C 包里面的整數(shù)原子類 AtomicInteger 的方法調(diào)用了 Unsafe 類的 CAS 操作呕童。
以下代碼使用了 AtomicInteger 執(zhí)行了自增的操作淆珊。
privateAtomicInteger cnt =newAtomicInteger();publicvoidadd(){? ? cnt.incrementAndGet();}
以下代碼是 incrementAndGet() 的源碼施符,它調(diào)用了 Unsafe 的 getAndAddInt() 戳吝。
publicfinalint incrementAndGet() {returnunsafe.getAndAddInt(this, valueOffset,1) +1;}
以下代碼是 getAndAddInt() 源碼,var1 指示對象內(nèi)存地址撼嗓,var2 指示該字段相對對象內(nèi)存地址的偏移欢唾,var4 指示操作需要加的數(shù)值礁遣,這里為 1。通過 getIntVolatile(var1, var2) 得到舊的預(yù)期值杏头,通過調(diào)用 compareAndSwapInt() 來進行 CAS 比較醇王,如果該字段內(nèi)存地址中的值等于 var5崭添,那么就更新內(nèi)存地址為 var1+var2 的變量為 var5+var4呼渣。
可以看到 getAndAddInt() 在一個循環(huán)中進行,發(fā)生沖突的做法是不斷的進行重試焊夸。
publicfinalintgetAndAddInt(Object var1,longvar2,intvar4){intvar5;do{? ? ? ? var5 =this.getIntVolatile(var1, var2);? ? }while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));returnvar5;}
③. ABA
如果一個變量初次讀取的時候是 A 值阱穗,它的值被改成了 B使鹅,后來又被改回為 A并徘,那 CAS 操作就會誤認為它從來沒有被改變過。
J.U.C 包提供了一個帶有標記的原子引用類 AtomicStampedReference 來解決這個問題蕴茴,它可以通過控制變量值的版本來保證 CAS 的正確性倦淀。大部分情況下 ABA 問題不會影響程序并發(fā)的正確性声畏,如果需要解決 ABA 問題蜻势,改用傳統(tǒng)的互斥同步可能會比原子類更高效糠雨。
4. 無同步方案
要保證線程安全,并不是一定就要進行同步琅攘。如果一個方法本來就不涉及共享數(shù)據(jù)坞琴,那它自然就無須任何同步措施去保證正確性逗抑。
①. 棧封閉
多個線程訪問同一個方法的局部變量時锋八,不會出現(xiàn)線程安全問題,因為局部變量存儲在虛擬機棧中羞酗,屬于線程私有的檀轨。
publicclassStackClosedExample{publicvoidadd100() {? ? ? ? int cnt =0;for(int i =0; i <100; i++) {? ? ? ? ? ? cnt++;? ? ? ? }? ? ? ? System.out.println(cnt);? ? }}public staticvoidmain(String[] args) {? ? StackClosedExample example =newStackClosedExample();? ? ExecutorService executorService = Executors.newCachedThreadPool();? ? executorService.execute(() -> example.add100());executorService.execute(() -> example.add100());executorService.shutdown();}
②. 線程本地存儲(Thread Local Storage)
如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享参萄,那就看看這些共享數(shù)據(jù)的代碼是否能保證在同一個線程中執(zhí)行煎饼。如果能保證吆玖,我們就可以把共享數(shù)據(jù)的可見范圍限制在同一個線程之內(nèi),這樣怜奖,無須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭用的問題歪玲。
符合這種特點的應(yīng)用并不少見,大部分使用消費隊列的架構(gòu)模式(如“生產(chǎn)者-消費者”模式)都會將產(chǎn)品的消費過程盡量在一個線程中消費完岖圈。其中最重要的一個應(yīng)用實例就是經(jīng)典 Web 交互模型中的“一個請求對應(yīng)一個服務(wù)器線程”(Thread-per-Request)的處理方式,這種處理方式的廣泛應(yīng)用使得很多 Web 服務(wù)端應(yīng)用都可以使用線程本地存儲來解決線程安全問題株灸。
可以使用 java.lang.ThreadLocal 類來實現(xiàn)線程本地存儲功能擎值。
對于以下代碼鸠儿,thread1 中設(shè)置 threadLocal 為 1进每,而 thread2 設(shè)置 threadLocal 為 2。過了一段時間之后嘱兼,thread1 讀取 threadLocal 依然是 1芹壕,不受 thread2 的影響接奈。
publicclassThreadLocalExample{public staticvoidmain(String[] args) {? ? ? ? ThreadLocal threadLocal =newThreadLocal();? ? ? ? Thread thread1 =newThread(() -> {? ? ? ? ? ? threadLocal.set(1);try{? ? ? ? ? ? ? ? Thread.sleep(1000);? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }? ? ? ? ? ? System.out.println(threadLocal.get());? ? ? ? ? ? threadLocal.remove();? ? ? ? });Threadthread2=newThread(() -> {? ? ? ? ? ? threadLocal.set(2);? ? ? ? ? ? threadLocal.remove();? ? ? ? });thread1.start();thread2.start();? ? }}
為了理解 ThreadLocal序宦,先看以下代碼:
publicclassThreadLocalExample1{public staticvoidmain(String[] args) {? ? ? ? ThreadLocal threadLocal1 =newThreadLocal();? ? ? ? ThreadLocal threadLocal2 =newThreadLocal();? ? ? ? Thread thread1 =newThread(() -> {? ? ? ? ? ? threadLocal1.set(1);? ? ? ? ? ? threadLocal2.set(1);? ? ? ? });Threadthread2=newThread(() -> {? ? ? ? ? ? threadLocal1.set(2);? ? ? ? ? ? threadLocal2.set(2);? ? ? ? });thread1.start();thread2.start();? ? }}
每個 Thread 都有一個 ThreadLocal.ThreadLocalMap 對象互捌。
/* ThreadLocal values pertainingtothis thread. This mapismaintained * by the ThreadLocalclass. */ThreadLocal.ThreadLocalMap threadLocals =null;
當調(diào)用一個 ThreadLocal 的 set(T value) 方法時疫剃,先得到當前線程的 ThreadLocalMap 對象巢价,然后將 ThreadLocal->value 鍵值對插入到該 Map 中固阁。
publicvoidset(Tvalue){? ? Thread t = Thread.currentThread();? ? ThreadLocalMap map = getMap(t);if(map !=null)? ? ? ? map.set(this,value);elsecreateMap(t,value);}
get() 方法類似备燃。
publicTget() {? ? Thread t = Thread.currentThread();? ? ThreadLocalMap map = getMap(t);if(map !=null) {? ? ? ? ThreadLocalMap.Entry e = map.getEntry(this);if(e !=null) {@SuppressWarnings("unchecked")T result = (T)e.value;returnresult;? ? ? ? }? ? }returnsetInitialValue();}
ThreadLocal 從理論上講并不是用來解決多線程并發(fā)問題的并齐,因為根本不存在多線程競爭客税。
在一些場景 (尤其是使用線程池) 下更耻,由于 ThreadLocal.ThreadLocalMap 的底層數(shù)據(jù)結(jié)構(gòu)導致 ThreadLocal 有內(nèi)存泄漏的情況秧均,應(yīng)該盡可能在每次使用 ThreadLocal 后手動調(diào)用 remove(),以避免出現(xiàn) ThreadLocal 經(jīng)典的內(nèi)存泄漏甚至是造成自身業(yè)務(wù)混亂的風險锯七。
③. 可重入代碼(Reentrant Code)
這種代碼也叫做純代碼(Pure Code)起胰,可以在代碼執(zhí)行的任何時刻中斷它效五,轉(zhuǎn)而去執(zhí)行另外一段代碼(包括遞歸調(diào)用它本身)炉峰,而在控制權(quán)返回后疼阔,原來的程序不會出現(xiàn)任何錯誤婆廊。
可重入代碼有一些共同的特征,例如不依賴于存儲在堆上的數(shù)據(jù)和公用的系統(tǒng)資源茵典、用到的狀態(tài)量都由參數(shù)中傳入统阿、不調(diào)用非可重入的方法等扶平。
二十八、多線程開發(fā)良好的實踐
給線程起個有意義的名字哥谷,這樣可以方便找 Bug们妥。
縮小同步范圍赎瑰,從而減少鎖爭用餐曼。例如對于 synchronized源譬,應(yīng)該盡量使用同步塊而不是同步方法踩娘。
多用同步工具少用 wait() 和 notify()喉祭。首先泛烙,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 這些同步類簡化了編碼操作蔽氨,而用 wait() 和 notify() 很難實現(xiàn)復雜控制流;其次宇立,這些同步類是由最好的企業(yè)編寫和維護妈嘹,在后續(xù)的 JDK 中還會不斷優(yōu)化和完善蟋滴。
使用 BlockingQueue 實現(xiàn)生產(chǎn)者消費者問題。
多用并發(fā)集合少用同步集合肖粮,例如應(yīng)該使用 ConcurrentHashMap 而不是 Hashtable涩馆。
使用本地變量和不可變類來保證線程安全魂那。
使用線程池而不是直接創(chuàng)建線程,這是因為創(chuàng)建線程代價很高稠项,線程池可以有效地利用有限的線程來啟動任務(wù)涯雅。