深入理解 Java 多線程核心知識:跳槽面試必備

多線程相對于其他 Java 知識點來講冕茅,有一定的學(xué)習(xí)門檻伤极,并且了解起來比較費勁。在平時工作中如若使用不當(dāng)會出現(xiàn)數(shù)據(jù)錯亂姨伤、執(zhí)行效率低(還不如單線程去運行)或者死鎖程序掛掉等等問題哨坪,所以掌握了解多線程至關(guān)重要。

本文從基礎(chǔ)概念開始到最后的并發(fā)模型由淺入深乍楚,講解下線程方面的知識当编。

概念梳理

本節(jié)我將帶大家了解多線程中幾大基礎(chǔ)概念。

并發(fā)與并行

并行徒溪,表示兩個線程同時做事情忿偷。

并發(fā)金顿,表示一會做這個事情,一會做另一個事情鲤桥,存在著調(diào)度揍拆。單核 CPU 不可能存在并行(微觀上)。

臨界區(qū)

臨界區(qū)用來表示一種公共資源或者說是共享數(shù)據(jù)茶凳,可以被多個線程使用嫂拴。但是每一次,只能有一個線程使用它贮喧,一旦臨界區(qū)資源被占用顷牌,其他線程要想使用這個資源,就必須等待塞淹。

阻塞與非阻塞

阻塞和非阻塞通常用來形容多線程間的相互影響。比如一個線程占用了臨界區(qū)資源罪裹,那么其它所有需要這個資源的線程就必須在這個臨界區(qū)中進行等待饱普,等待會導(dǎo)致線程掛起。這種情況就是阻塞状共。

此時套耕,如果占用資源的線程一直不愿意釋放資源,那么其它所有阻塞在這個臨界區(qū)上的線程都不能工作峡继。阻塞是指線程在操作系統(tǒng)層面被掛起冯袍。阻塞一般性能不好,需大約8萬個時鐘周期來做調(diào)度碾牌。

非阻塞則允許多個線程同時進入臨界區(qū)康愤。

死鎖

死鎖是進程死鎖的簡稱,是指多個進程循環(huán)等待他方占有的資源而無限的僵持下去的局面舶吗。

活鎖

假設(shè)有兩個線程1征冷、2,它們都需要資源 A/B誓琼,假設(shè)1號線程占有了 A 資源检激,2號線程占有了 B 資源;由于兩個線程都需要同時擁有這兩個資源才可以工作腹侣,為了避免死鎖叔收,1號線程釋放了 A 資源占有鎖,2號線程釋放了 B 資源占有鎖傲隶;此時 AB 空閑饺律,兩個線程又同時搶鎖,再次出現(xiàn)上述情況跺株,此時發(fā)生了活鎖蓝晒。

簡單類比腮出,電梯遇到人,一個進的一個出的芝薇,對面占路胚嘲,兩個人同時往一個方向讓路,來回重復(fù)洛二,還是堵著路馋劈。

如果線上應(yīng)用遇到了活鎖問題,恭喜你中獎了晾嘶,這類問題比較難排查妓雾。

饑餓

饑餓是指某一個或者多個線程因為種種原因無法獲得所需要的資源,導(dǎo)致一直無法執(zhí)行垒迂。

線程的生命周期

在線程的生命周期中械姻,它要經(jīng)歷創(chuàng)建、可運行机断、不可運行幾種狀態(tài)楷拳。

創(chuàng)建狀態(tài)

當(dāng)用 new 操作符創(chuàng)建一個新的線程對象時,該線程處于創(chuàng)建狀態(tài)吏奸。

處于創(chuàng)建狀態(tài)的線程只是一個空的線程對象欢揖,系統(tǒng)不為它分配資源。

可運行狀態(tài)

執(zhí)行線程的 start() 方法將為線程分配必須的系統(tǒng)資源奋蔚,安排其運行她混,并調(diào)用線程體——run()方法,這樣就使得該線程處于可運行狀態(tài)(Runnable)泊碑。

這一狀態(tài)并不是運行中狀態(tài)(Running)坤按,因為線程也許實際上并未真正運行。

不可運行狀態(tài)

當(dāng)發(fā)生下列事件時馒过,處于運行狀態(tài)的線程會轉(zhuǎn)入到不可運行狀態(tài):

  • 調(diào)用了 sleep() 方法晋涣;
  • 線程調(diào)用 wait() 方法等待特定條件的滿足;
  • 線程輸入/輸出阻塞沉桌;
  • 返回可運行狀態(tài)谢鹊;
  • 處于睡眠狀態(tài)的線程在指定的時間過去后;
  • 如果線程在等待某一條件留凭,另一個對象必須通過 notify() 或 notifyAll() 方法通知等待線程條件的改變佃扼;
  • 如果線程是因為輸入輸出阻塞,等待輸入輸出完成蔼夜。

線程的優(yōu)先級

線程優(yōu)先級及設(shè)置

線程的優(yōu)先級是為了在多線程環(huán)境中便于系統(tǒng)對線程的調(diào)度兼耀,優(yōu)先級高的線程將優(yōu)先執(zhí)行。一個線程的優(yōu)先級設(shè)置遵從以下原則:

  • 線程創(chuàng)建時,子繼承父的優(yōu)先級瘤运;
  • 線程創(chuàng)建后窍霞,可通過調(diào)用 setPriority() 方法改變優(yōu)先級;
  • 線程的優(yōu)先級是1-10之間的正整數(shù)拯坟。

線程的調(diào)度策略

線程調(diào)度器選擇優(yōu)先級最高的線程運行但金。但是,如果發(fā)生以下情況郁季,就會終止線程的運行:

  • 線程體中調(diào)用了 yield() 方法冷溃,讓出了對 CPU 的占用權(quán);
  • 線程體中調(diào)用了 sleep() 方法梦裂,使線程進入睡眠狀態(tài)似枕;
  • 線程由于 I/O 操作而受阻塞;
  • 另一個更高優(yōu)先級的線程出現(xiàn)年柠;
  • 在支持時間片的系統(tǒng)中凿歼,該線程的時間片用完。

單線程創(chuàng)建方式

單線程創(chuàng)建方式比較簡單冗恨,一般只有兩種方式:繼承 Thread 類和實現(xiàn) Runnable 接口答憔;這兩種方式比較常用就不在 Demo 了,但是對于新手需要注意的問題有:

  • 不管是繼承 Thread 類還是實現(xiàn) Runable 接口派近,業(yè)務(wù)邏輯是寫在 run 方法里面,線程啟動的時候是執(zhí)行 start() 方法洁桌;
  • 開啟新的線程渴丸,不影響主線程的代碼執(zhí)行順序也不會阻塞主線程的執(zhí)行;
  • 新的線程和主線程的代碼執(zhí)行順序是不能夠保證先后的另凌;
  • 對于多線程程序谱轨,從微觀上來講某一時刻只有一個線程在工作,多線程目的是讓 CPU 忙起來吠谢;
  • 通過查看 Thread 的源碼可以看到土童,Thread 類是實現(xiàn)了 Runnable 接口的,所以這兩種本質(zhì)上來講是一個工坊;

PS:平時在工作中也可以借鑒這種代碼結(jié)構(gòu)献汗,對上層調(diào)用來講提供更多的選擇,作為服務(wù)提供方核心業(yè)務(wù)歸一維護

為什么要用線程池

通過上面的介紹王污,完全可以開發(fā)一個多線程的程序罢吃,為什么還要引入線程池呢。主要是因為上述單線程方式存在以下幾個問題:

  • 線程的工作周期:線程創(chuàng)建所需時間為 T1昭齐,線程執(zhí)行任務(wù)所需時間為 T2尿招,線程銷毀所需時間為 T3,往往是 T1+T3 大于 T2,所有如果頻繁創(chuàng)建線程會損耗過多額外的時間就谜;
  • 如果有任務(wù)來了怪蔑,再去創(chuàng)建線程的話效率比較低,如果從一個池子中可以直接獲取可用的線程丧荐,那效率會有所提高缆瓣。所以線程池省去了任務(wù)過來,要先創(chuàng)建線程再去執(zhí)行的過程篮奄,節(jié)省了時間捆愁,提升了效率;
  • 線程池可以管理和控制線程窟却,因為線程是稀缺資源昼丑,如果無限制的創(chuàng)建,不僅會消耗系統(tǒng)資源夸赫,還會降低系統(tǒng)的穩(wěn)定性菩帝,使用線程池可以進行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控茬腿;
  • 線程池提供隊列呼奢,存放緩沖等待執(zhí)行的任務(wù)。

大致總結(jié)了上述的幾個原因切平,所以可以得出一個結(jié)論就是在平時工作中握础,如果要開發(fā)多線程程序,盡量要使用線程池的方式來創(chuàng)建和管理線程悴品。

通過線程池創(chuàng)建線程從調(diào)用 API 角度來說分為兩種禀综,一種是原生的線程池,另外該一種是通過 Java 提供的并發(fā)包來創(chuàng)建苔严,后者比較簡單定枷,后者其實是對原生的線程池創(chuàng)建方式做了一次簡化包裝,讓調(diào)用者使用起來更方便届氢,但道理都是一樣的欠窒。所以搞明白原生線程池的原理是非常重要的。

ThreadPoolExecutor

通過 ThreadPoolExecutor 創(chuàng)建線程池退子,API 如下所示:

public ThreadPoolExecutor(int corePoolSize,                          int maximumPoolSize,                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue); 

先來解釋下其中的參數(shù)含義(如果看的比較模糊可以大致有個印象岖妄,后面的圖是關(guān)鍵)。

  • corePoolSize
  • 核心池的大小寂祥。

在創(chuàng)建了線程池后衣吠,默認情況下,線程池中并沒有任何線程壤靶,而是等待有任務(wù)到來才創(chuàng)建線程去執(zhí)行任務(wù)缚俏,除非調(diào)用了 prestartAllCoreThreads() 或者 prestartCoreThread() 方法,從這兩個方法的名字就可以看出,是預(yù)創(chuàng)建線程的意思忧换,即在沒有任務(wù)到來之前就創(chuàng)建 corePoolSize 個線程或者一個線程恬惯。默認情況下,在創(chuàng)建了線程池后亚茬,線程池中的線程數(shù)為0酪耳,當(dāng)有任務(wù)來之后,就會創(chuàng)建一個線程去執(zhí)行任務(wù)刹缝,當(dāng)線程池中的線程數(shù)目達到 corePoolSize 后碗暗,就會把到達的任務(wù)放到緩存隊列當(dāng)中。

  • maximumPoolSize

線程池最大線程數(shù)梢夯,這個參數(shù)也是一個非常重要的參數(shù)言疗,它表示在線程池中最多能創(chuàng)建多少個線程。

  • keepAliveTime

表示線程沒有任務(wù)執(zhí)行時最多保持多久時間會終止颂砸。默認情況下噪奄,只有當(dāng)線程池中的線程數(shù)大于 corePoolSize 時,keepAliveTime 才會起作用人乓,直到線程池中的線程數(shù)不大于 corePoolSize勤篮,即當(dāng)線程池中的線程數(shù)大于 corePoolSize 時,如果一個線程空閑的時間達到 keepAliveTime色罚,則會終止碰缔,直到線程池中的線程數(shù)不超過 corePoolSize。

但是如果調(diào)用了 allowCoreThreadTimeOut(boolean) 方法戳护,在線程池中的線程數(shù)不大于 corePoolSize 時金抡,keepAliveTime 參數(shù)也會起作用,直到線程池中的線程數(shù)為0姑尺。

  • unit

參數(shù) keepAliveTime 的時間單位竟终。

  • workQueue

一個阻塞隊列蝠猬,用來存儲等待執(zhí)行的任務(wù)切蟋,這個參數(shù)的選擇也很重要,會對線程池的運行過程產(chǎn)生重大影響榆芦,一般來說柄粹,這里的阻塞隊列有以下這幾種選擇:ArrayBlockingQueue、LinkedBlockingQueue匆绣、SynchronousQueue驻右。

  • threadFactory

線程工廠,主要用來創(chuàng)建線程崎淳。

  • handler

表示當(dāng)拒絕處理任務(wù)時的策略堪夭,有以下四種取值:

  1. ThreadPoolExecutor.AbortPolicy:丟棄任務(wù)并拋出 RejectedExecutionException 異常;
  2. ThreadPoolExecutor.DiscardPolicy:也是丟棄任務(wù),但是不拋出異常森爽;
  3. ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務(wù)恨豁,然后重新嘗試執(zhí)行任務(wù)(重復(fù)此過程);
  4. ThreadPoolExecutor.CallerRunsPolicy:由調(diào)用線程處理該任務(wù)爬迟。

上面這些參數(shù)是如何配合工作的呢橘蜜?請看下圖:

注意圖上面的序號。

簡單總結(jié)下線程池之間的參數(shù)協(xié)作分為以下幾步:

  1. 線程優(yōu)先向 CorePool 中提交付呕;
  2. 在 Corepool 滿了之后计福,線程被提交到任務(wù)隊列,等待線程池空閑徽职;
  3. 在任務(wù)隊列滿了之后 corePool 還沒有空閑象颖,那么任務(wù)將被提交到 maxPool 中,如果 MaxPool 滿了之后執(zhí)行 task 拒絕策略活箕。

流程圖如下:

image

以上就是原生線程池創(chuàng)建的核心原理力麸。除了原生線程池之外并發(fā)包還提供了簡單的創(chuàng)建方式,上面也說了它們是對原生線程池的一種包裝育韩,可以讓開發(fā)者簡單快捷的創(chuàng)建所需要的線程池克蚂。

Executors

newSingleThreadExecutor

創(chuàng)建一個線程的線程池,在這個線程池中始終只有一個線程存在筋讨。如果線程池中的線程因為異常問題退出埃叭,那么會有一個新的線程來替代它。此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行悉罕。

newFixedThreadPool

創(chuàng)建固定大小的線程池赤屋。每次提交一個任務(wù)就創(chuàng)建一個線程,直到線程達到線程池的最大大小壁袄。線程池的大小一旦達到最大值就會保持不變类早,如果某個線程因為執(zhí)行異常而結(jié)束,那么線程池會補充一個新線程嗜逻。

newCachedThreadPool

可根據(jù)實際情況涩僻,調(diào)整線程數(shù)量的線程池,線程池中的線程數(shù)量不確定栈顷,如果有空閑線程會優(yōu)先選擇空閑線程逆日,如果沒有空閑線程并且此時有任務(wù)提交會創(chuàng)建新的線程。在正常開發(fā)中并不推薦這個線程池萄凤,因為在極端情況下室抽,會因為 newCachedThreadPool 創(chuàng)建過多線程而耗盡 CPU 和內(nèi)存資源。

newScheduledThreadPool

此線程池可以指定固定數(shù)量的線程來周期性的去執(zhí)行靡努。比如通過 scheduleAtFixedRate 或者 scheduleWithFixedDelay 來指定周期時間坪圾。

PS:另外在寫定時任務(wù)時(如果不用 Quartz 框架)晓折,最好采用這種線程池來做,因為它可以保證里面始終是存在活的線程的兽泄。

推薦使用 ThreadPoolExecutor 方式

在阿里的 Java 開發(fā)手冊時有一條是不推薦使用 Executors 去創(chuàng)建已维,而是推薦去使用 ThreadPoolExecutor 來創(chuàng)建線程池昌简。

這樣做的目的主要原因是:使用 Executors 創(chuàng)建線程池不會傳入核心參數(shù)跨细,而是采用的默認值,這樣的話我們往往會忽略掉里面參數(shù)的含義啸箫,如果業(yè)務(wù)場景要求比較苛刻的話飘千,存在資源耗盡的風(fēng)險堂鲜;另外采用 ThreadPoolExecutor 的方式可以讓我們更加清楚地了解線程池的運行規(guī)則,不管是面試還是對技術(shù)成長都有莫大的好處护奈。

改了變量缔莲,其他線程可以立即知道。保證可見性的方法有以下幾種:

  • volatile

加入 volatile 關(guān)鍵字的變量在進行匯編時會多出一個 lock 前綴指令霉旗,這個前綴指令相當(dāng)于一個內(nèi)存屏障痴奏,內(nèi)存屏障可以保證內(nèi)存操作的順序。當(dāng)聲明為 volatile 的變量進行寫操作時厌秒,那么這個變量需要將數(shù)據(jù)寫到主內(nèi)存中读拆。

由于處理器會實現(xiàn)緩存一致性協(xié)議,所以寫到主內(nèi)存后會導(dǎo)致其他處理器的緩存無效鸵闪,也就是線程工作內(nèi)存無效檐晕,需要從主內(nèi)存中重新刷新數(shù)據(jù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚌讼,一起剝皮案震驚了整個濱河市辟灰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌篡石,老刑警劉巖芥喇,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異凰萨,居然都是意外死亡继控,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門沟蔑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來湿诊,“玉大人狱杰,你說我怎么就攤上這事瘦材。” “怎么了仿畸?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵朗和,是天一觀的道長。 經(jīng)常有香客問我簿晓,道長眶拉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任憔儿,我火速辦了婚禮忆植,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谒臼。我一直安慰自己朝刊,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布蜈缤。 她就那樣靜靜地躺著拾氓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪底哥。 梳的紋絲不亂的頭發(fā)上咙鞍,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機與錄音趾徽,去河邊找鬼续滋。 笑死,一個胖子當(dāng)著我的面吹牛孵奶,可吹牛的內(nèi)容都是我干的吃粒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拒课,長吁一口氣:“原來是場噩夢啊……” “哼徐勃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起早像,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤僻肖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后卢鹦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臀脏,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年冀自,在試婚紗的時候發(fā)現(xiàn)自己被綠了揉稚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡熬粗,死狀恐怖搀玖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情驻呐,我是刑警寧澤灌诅,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布芳来,位于F島的核電站,受9級特大地震影響猜拾,放射性物質(zhì)發(fā)生泄漏即舌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一挎袜、第九天 我趴在偏房一處隱蔽的房頂上張望顽聂。 院中可真熱鬧,春花似錦盯仪、人聲如沸芜飘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗦明。三九已至,卻和暖如春蚪燕,著一層夾襖步出監(jiān)牢的瞬間娶牌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工馆纳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诗良,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓鲁驶,卻偏偏與公主長得像鉴裹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子钥弯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 多線程相對于其他 Java 知識點來講径荔,有一定的學(xué)習(xí)門檻,并且了解起來比較費勁脆霎。在平時工作中如若使用不當(dāng)會出現(xiàn)數(shù)據(jù)...
    a6fc544968bb閱讀 487評論 1 3
  • 導(dǎo)語:多線程相對于其他 Java 知識點來講总处,有一定的學(xué)習(xí)門檻,并且了解起來比較費勁睛蛛。在平時工作中如若使用不當(dāng)會出...
    程序員技術(shù)圈閱讀 1,037評論 0 48
  • 為什么使用線程池 當(dāng)我們在使用線程時鹦马,如果每次需要一個線程時都去創(chuàng)建一個線程,這樣實現(xiàn)起來很簡單忆肾,但是會有一個問題...
    閩越布衣閱讀 4,291評論 10 45
  • 【JAVA 線程】 線程 進程:是一個正在執(zhí)行中的程序荸频。每一個進程執(zhí)行都有一個執(zhí)行順序。該順序是一個執(zhí)行路徑客冈,或者...
    Rtia閱讀 2,766評論 2 20
  • 正因為喜歡旭从,所以才會愛屋及烏。愛他郊酒,和有他在的城遇绞。可是如果中途放棄了這份喜歡燎窘,那還會愛這座城嗎摹闽? 這座城里藏有...
    一周一說閱讀 439評論 0 1