新聞中心
多線(xiàn)程相對(duì)于其他 Java 知識(shí)點(diǎn)來(lái)講,有一定的學(xué)習(xí)門(mén)檻,并且了解起來(lái)比較費(fèi)勁。在平時(shí)工作中如若使用不當(dāng)會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)亂、執(zhí)行效率低(還不如單線(xiàn)程去運(yùn)行)或者死鎖程序掛掉等等問(wèn)題,所以掌握了解多線(xiàn)程至關(guān)重要。

創(chuàng)新互聯(lián)建站是一家集網(wǎng)站建設(shè),連云港企業(yè)網(wǎng)站建設(shè),連云港品牌網(wǎng)站建設(shè),網(wǎng)站定制,連云港網(wǎng)站建設(shè)報(bào)價(jià),網(wǎng)絡(luò)營(yíng)銷(xiāo),網(wǎng)絡(luò)優(yōu)化,連云港網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競(jìng)爭(zhēng)力。可充分滿(mǎn)足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時(shí)我們時(shí)刻保持專(zhuān)業(yè)、時(shí)尚、前沿,時(shí)刻以成就客戶(hù)成長(zhǎng)自我,堅(jiān)持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。
本文從基礎(chǔ)概念開(kāi)始到***的并發(fā)模型由淺入深,講解下線(xiàn)程方面的知識(shí)。
概念梳理
本節(jié)我將帶大家了解多線(xiàn)程中幾大基礎(chǔ)概念。
并發(fā)與并行
并行,表示兩個(gè)線(xiàn)程同時(shí)做事情。
并發(fā),表示一會(huì)做這個(gè)事情,一會(huì)做另一個(gè)事情,存在著調(diào)度。單核 CPU 不可能存在并行(微觀(guān)上)。
臨界區(qū)
臨界區(qū)用來(lái)表示一種公共資源或者說(shuō)是共享數(shù)據(jù),可以被多個(gè)線(xiàn)程使用。但是每一次,只能有一個(gè)線(xiàn)程使用它,一旦臨界區(qū)資源被占用,其他線(xiàn)程要想使用這個(gè)資源,就必須等待。
阻塞與非阻塞
阻塞和非阻塞通常用來(lái)形容多線(xiàn)程間的相互影響。比如一個(gè)線(xiàn)程占用了臨界區(qū)資源,那么其它所有需要這個(gè)資源的線(xiàn)程就必須在這個(gè)臨界區(qū)中進(jìn)行等待,等待會(huì)導(dǎo)致線(xiàn)程掛起。這種情況就是阻塞。
此時(shí),如果占用資源的線(xiàn)程一直不愿意釋放資源,那么其它所有阻塞在這個(gè)臨界區(qū)上的線(xiàn)程都不能工作。阻塞是指線(xiàn)程在操作系統(tǒng)層面被掛起。阻塞一般性能不好,需大約8萬(wàn)個(gè)時(shí)鐘周期來(lái)做調(diào)度。
非阻塞則允許多個(gè)線(xiàn)程同時(shí)進(jìn)入臨界區(qū)。
死鎖
死鎖是進(jìn)程死鎖的簡(jiǎn)稱(chēng),是指多個(gè)進(jìn)程循環(huán)等待他方占有的資源而***的僵持下去的局面。
活鎖
假設(shè)有兩個(gè)線(xiàn)程1、2,它們都需要資源 A/B,假設(shè)1號(hào)線(xiàn)程占有了 A 資源,2號(hào)線(xiàn)程占有了 B 資源;由于兩個(gè)線(xiàn)程都需要同時(shí)擁有這兩個(gè)資源才可以工作,為了避免死鎖,1號(hào)線(xiàn)程釋放了 A 資源占有鎖,2號(hào)線(xiàn)程釋放了 B 資源占有鎖;此時(shí) AB 空閑,兩個(gè)線(xiàn)程又同時(shí)搶鎖,再次出現(xiàn)上述情況,此時(shí)發(fā)生了活鎖。
簡(jiǎn)單類(lèi)比,電梯遇到人,一個(gè)進(jìn)的一個(gè)出的,對(duì)面占路,兩個(gè)人同時(shí)往一個(gè)方向讓路,來(lái)回重復(fù),還是堵著路。
如果線(xiàn)上應(yīng)用遇到了活鎖問(wèn)題,恭喜你中獎(jiǎng)了,這類(lèi)問(wèn)題比較難排查。
饑餓
饑餓是指某一個(gè)或者多個(gè)線(xiàn)程因?yàn)榉N種原因無(wú)法獲得所需要的資源,導(dǎo)致一直無(wú)法執(zhí)行。
線(xiàn)程的生命周期
在線(xiàn)程的生命周期中,它要經(jīng)歷創(chuàng)建、可運(yùn)行、不可運(yùn)行幾種狀態(tài)。
創(chuàng)建狀態(tài)
當(dāng)用 new 操作符創(chuàng)建一個(gè)新的線(xiàn)程對(duì)象時(shí),該線(xiàn)程處于創(chuàng)建狀態(tài)。
處于創(chuàng)建狀態(tài)的線(xiàn)程只是一個(gè)空的線(xiàn)程對(duì)象,系統(tǒng)不為它分配資源。
可運(yùn)行狀態(tài)
執(zhí)行線(xiàn)程的 start() 方法將為線(xiàn)程分配必須的系統(tǒng)資源,安排其運(yùn)行,并調(diào)用線(xiàn)程體——run()方法,這樣就使得該線(xiàn)程處于可運(yùn)行狀態(tài)(Runnable)。
這一狀態(tài)并不是運(yùn)行中狀態(tài)(Running),因?yàn)榫€(xiàn)程也許實(shí)際上并未真正運(yùn)行。
不可運(yùn)行狀態(tài)
當(dāng)發(fā)生下列事件時(shí),處于運(yùn)行狀態(tài)的線(xiàn)程會(huì)轉(zhuǎn)入到不可運(yùn)行狀態(tài):
- 調(diào)用了 sleep() 方法;
- 線(xiàn)程調(diào)用 wait() 方法等待特定條件的滿(mǎn)足;
- 線(xiàn)程輸入/輸出阻塞;
- 返回可運(yùn)行狀態(tài);
- 處于睡眠狀態(tài)的線(xiàn)程在指定的時(shí)間過(guò)去后;
- 如果線(xiàn)程在等待某一條件,另一個(gè)對(duì)象必須通過(guò) notify() 或 notifyAll() 方法通知等待線(xiàn)程條件的改變;
- 如果線(xiàn)程是因?yàn)檩斎胼敵鲎枞?,等待輸入輸出完成?/li>
線(xiàn)程的優(yōu)先級(jí)
線(xiàn)程優(yōu)先級(jí)及設(shè)置
線(xiàn)程的優(yōu)先級(jí)是為了在多線(xiàn)程環(huán)境中便于系統(tǒng)對(duì)線(xiàn)程的調(diào)度,優(yōu)先級(jí)高的線(xiàn)程將優(yōu)先執(zhí)行。一個(gè)線(xiàn)程的優(yōu)先級(jí)設(shè)置遵從以下原則:
- 線(xiàn)程創(chuàng)建時(shí),子繼承父的優(yōu)先級(jí);
- 線(xiàn)程創(chuàng)建后,可通過(guò)調(diào)用 setPriority() 方法改變優(yōu)先級(jí);
- 線(xiàn)程的優(yōu)先級(jí)是1-10之間的正整數(shù)。
線(xiàn)程的調(diào)度策略
線(xiàn)程調(diào)度器選擇優(yōu)先級(jí)***的線(xiàn)程運(yùn)行。但是,如果發(fā)生以下情況,就會(huì)終止線(xiàn)程的運(yùn)行:
- 線(xiàn)程體中調(diào)用了 yield() 方法,讓出了對(duì) CPU 的占用權(quán);
- 線(xiàn)程體中調(diào)用了 sleep() 方法,使線(xiàn)程進(jìn)入睡眠狀態(tài);
- 線(xiàn)程由于 I/O 操作而受阻塞;
- 另一個(gè)更高優(yōu)先級(jí)的線(xiàn)程出現(xiàn);
- 在支持時(shí)間片的系統(tǒng)中,該線(xiàn)程的時(shí)間片用完。
單線(xiàn)程創(chuàng)建方式
單線(xiàn)程創(chuàng)建方式比較簡(jiǎn)單,一般只有兩種方式:繼承 Thread 類(lèi)和實(shí)現(xiàn) Runnable 接口;這兩種方式比較常用就不在 Demo 了,但是對(duì)于新手需要注意的問(wèn)題有:
- 不管是繼承 Thread 類(lèi)還是實(shí)現(xiàn) Runable 接口,業(yè)務(wù)邏輯是寫(xiě)在 run 方法里面,線(xiàn)程啟動(dòng)的時(shí)候是執(zhí)行 start() 方法;
- 開(kāi)啟新的線(xiàn)程,不影響主線(xiàn)程的代碼執(zhí)行順序也不會(huì)阻塞主線(xiàn)程的執(zhí)行;
- 新的線(xiàn)程和主線(xiàn)程的代碼執(zhí)行順序是不能夠保證先后的;
- 對(duì)于多線(xiàn)程程序,從微觀(guān)上來(lái)講某一時(shí)刻只有一個(gè)線(xiàn)程在工作,多線(xiàn)程目的是讓 CPU 忙起來(lái);
- 通過(guò)查看 Thread 的源碼可以看到,Thread 類(lèi)是實(shí)現(xiàn)了 Runnable 接口的,所以這兩種本質(zhì)上來(lái)講是一個(gè);
PS:平時(shí)在工作中也可以借鑒這種代碼結(jié)構(gòu),對(duì)上層調(diào)用來(lái)講提供更多的選擇,作為服務(wù)提供方核心業(yè)務(wù)歸一維護(hù)
為什么要用線(xiàn)程池
通過(guò)上面的介紹,完全可以開(kāi)發(fā)一個(gè)多線(xiàn)程的程序,為什么還要引入線(xiàn)程池呢。主要是因?yàn)樯鲜鰡尉€(xiàn)程方式存在以下幾個(gè)問(wèn)題:
- 線(xiàn)程的工作周期:線(xiàn)程創(chuàng)建所需時(shí)間為 T1,線(xiàn)程執(zhí)行任務(wù)所需時(shí)間為 T2,線(xiàn)程銷(xiāo)毀所需時(shí)間為 T3,往往是 T1+T3 大于 T2,所有如果頻繁創(chuàng)建線(xiàn)程會(huì)損耗過(guò)多額外的時(shí)間;
- 如果有任務(wù)來(lái)了,再去創(chuàng)建線(xiàn)程的話(huà)效率比較低,如果從一個(gè)池子中可以直接獲取可用的線(xiàn)程,那效率會(huì)有所提高。所以線(xiàn)程池省去了任務(wù)過(guò)來(lái),要先創(chuàng)建線(xiàn)程再去執(zhí)行的過(guò)程,節(jié)省了時(shí)間,提升了效率;
- 線(xiàn)程池可以管理和控制線(xiàn)程,因?yàn)榫€(xiàn)程是稀缺資源,如果***制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線(xiàn)程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控;
- 線(xiàn)程池提供隊(duì)列,存放緩沖等待執(zhí)行的任務(wù)。
大致總結(jié)了上述的幾個(gè)原因,所以可以得出一個(gè)結(jié)論就是在平時(shí)工作中,如果要開(kāi)發(fā)多線(xiàn)程程序,盡量要使用線(xiàn)程池的方式來(lái)創(chuàng)建和管理線(xiàn)程。
通過(guò)線(xiàn)程池創(chuàng)建線(xiàn)程從調(diào)用 API 角度來(lái)說(shuō)分為兩種,一種是原生的線(xiàn)程池,另外該一種是通過(guò) Java 提供的并發(fā)包來(lái)創(chuàng)建,后者比較簡(jiǎn)單,后者其實(shí)是對(duì)原生的線(xiàn)程池創(chuàng)建方式做了一次簡(jiǎn)化包裝,讓調(diào)用者使用起來(lái)更方便,但道理都是一樣的。所以搞明白原生線(xiàn)程池的原理是非常重要的。
ThreadPoolExecutor
通過(guò) ThreadPoolExecutor 創(chuàng)建線(xiàn)程池,API 如下所示:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit,
BlockingQueue
先來(lái)解釋下其中的參數(shù)含義(如果看的比較模糊可以大致有個(gè)印象,后面的圖是關(guān)鍵)。
- corePoolSize
- 核心池的大小。
在創(chuàng)建了線(xiàn)程池后,默認(rèn)情況下,線(xiàn)程池中并沒(méi)有任何線(xiàn)程,而是等待有任務(wù)到來(lái)才創(chuàng)建線(xiàn)程去執(zhí)行任務(wù),除非調(diào)用了 prestartAllCoreThreads() 或者 prestartCoreThread() 方法,從這兩個(gè)方法的名字就可以看出,是預(yù)創(chuàng)建線(xiàn)程的意思,即在沒(méi)有任務(wù)到來(lái)之前就創(chuàng)建 corePoolSize 個(gè)線(xiàn)程或者一個(gè)線(xiàn)程。默認(rèn)情況下,在創(chuàng)建了線(xiàn)程池后,線(xiàn)程池中的線(xiàn)程數(shù)為0,當(dāng)有任務(wù)來(lái)之后,就會(huì)創(chuàng)建一個(gè)線(xiàn)程去執(zhí)行任務(wù),當(dāng)線(xiàn)程池中的線(xiàn)程數(shù)目達(dá)到 corePoolSize 后,就會(huì)把到達(dá)的任務(wù)放到緩存隊(duì)列當(dāng)中。
- maximumPoolSize
線(xiàn)程池***線(xiàn)程數(shù),這個(gè)參數(shù)也是一個(gè)非常重要的參數(shù),它表示在線(xiàn)程池中最多能創(chuàng)建多少個(gè)線(xiàn)程。
- keepAliveTime
表示線(xiàn)程沒(méi)有任務(wù)執(zhí)行時(shí)最多保持多久時(shí)間會(huì)終止。默認(rèn)情況下,只有當(dāng)線(xiàn)程池中的線(xiàn)程數(shù)大于 corePoolSize 時(shí),keepAliveTime 才會(huì)起作用,直到線(xiàn)程池中的線(xiàn)程數(shù)不大于 corePoolSize,即當(dāng)線(xiàn)程池中的線(xiàn)程數(shù)大于 corePoolSize 時(shí),如果一個(gè)線(xiàn)程空閑的時(shí)間達(dá)到 keepAliveTime,則會(huì)終止,直到線(xiàn)程池中的線(xiàn)程數(shù)不超過(guò) corePoolSize。
但是如果調(diào)用了 allowCoreThreadTimeOut(boolean) 方法,在線(xiàn)程池中的線(xiàn)程數(shù)不大于 corePoolSize 時(shí),keepAliveTime 參數(shù)也會(huì)起作用,直到線(xiàn)程池中的線(xiàn)程數(shù)為0。
- unit
參數(shù) keepAliveTime 的時(shí)間單位。
- workQueue
一個(gè)阻塞隊(duì)列,用來(lái)存儲(chǔ)等待執(zhí)行的任務(wù),這個(gè)參數(shù)的選擇也很重要,會(huì)對(duì)線(xiàn)程池的運(yùn)行過(guò)程產(chǎn)生重大影響,一般來(lái)說(shuō),這里的阻塞隊(duì)列有以下這幾種選擇:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。
- threadFactory
線(xiàn)程工廠(chǎng),主要用來(lái)創(chuàng)建線(xiàn)程。
- handler
表示當(dāng)拒絕處理任務(wù)時(shí)的策略,有以下四種取值:
- ThreadPoolExecutor.AbortPolicy:丟棄任務(wù)并拋出 RejectedExecutionException 異常;
- ThreadPoolExecutor.DiscardPolicy:也是丟棄任務(wù),但是不拋出異常;
- ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊(duì)列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)(重復(fù)此過(guò)程);
- ThreadPoolExecutor.CallerRunsPolicy:由調(diào)用線(xiàn)程處理該任務(wù)。
上面這些參數(shù)是如何配合工作的呢?請(qǐng)看下圖:
注意圖上面的序號(hào)。
簡(jiǎn)單總結(jié)下線(xiàn)程池之間的參數(shù)協(xié)作分為以下幾步:
- 線(xiàn)程優(yōu)先向 CorePool 中提交;
- 在 Corepool 滿(mǎn)了之后,線(xiàn)程被提交到任務(wù)隊(duì)列,等待線(xiàn)程池空閑;
- 在任務(wù)隊(duì)列滿(mǎn)了之后 corePool 還沒(méi)有空閑,那么任務(wù)將被提交到 maxPool 中,如果 MaxPool 滿(mǎn)了之后執(zhí)行 task 拒絕策略。
流程圖如下:
以上就是原生線(xiàn)程池創(chuàng)建的核心原理。除了原生線(xiàn)程池之外并發(fā)包還提供了簡(jiǎn)單的創(chuàng)建方式,上面也說(shuō)了它們是對(duì)原生線(xiàn)程池的一種包裝,可以讓開(kāi)發(fā)者簡(jiǎn)單快捷的創(chuàng)建所需要的線(xiàn)程池。
Executors
newSingleThreadExecutor
創(chuàng)建一個(gè)線(xiàn)程的線(xiàn)程池,在這個(gè)線(xiàn)程池中始終只有一個(gè)線(xiàn)程存在。如果線(xiàn)程池中的線(xiàn)程因?yàn)楫惓?wèn)題退出,那么會(huì)有一個(gè)新的線(xiàn)程來(lái)替代它。此線(xiàn)程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行。
newFixedThreadPool
創(chuàng)建固定大小的線(xiàn)程池。每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線(xiàn)程,直到線(xiàn)程達(dá)到線(xiàn)程池的***大小。線(xiàn)程池的大小一旦達(dá)到***值就會(huì)保持不變,如果某個(gè)線(xiàn)程因?yàn)閳?zhí)行異常而結(jié)束,那么線(xiàn)程池會(huì)補(bǔ)充一個(gè)新線(xiàn)程。
newCachedThreadPool
可根據(jù)實(shí)際情況,調(diào)整線(xiàn)程數(shù)量的線(xiàn)程池,線(xiàn)程池中的線(xiàn)程數(shù)量不確定,如果有空閑線(xiàn)程會(huì)優(yōu)先選擇空閑線(xiàn)程,如果沒(méi)有空閑線(xiàn)程并且此時(shí)有任務(wù)提交會(huì)創(chuàng)建新的線(xiàn)程。在正常開(kāi)發(fā)中并不推薦這個(gè)線(xiàn)程池,因?yàn)樵跇O端情況下,會(huì)因?yàn)?newCachedThreadPool 創(chuàng)建過(guò)多線(xiàn)程而耗盡 CPU 和內(nèi)存資源。
newScheduledThreadPool
此線(xiàn)程池可以指定固定數(shù)量的線(xiàn)程來(lái)周期性的去執(zhí)行。比如通過(guò) scheduleAtFixedRate 或者 scheduleWithFixedDelay 來(lái)指定周期時(shí)間。
PS:另外在寫(xiě)定時(shí)任務(wù)時(shí)(如果不用 Quartz 框架),***采用這種線(xiàn)程池來(lái)做,因?yàn)樗梢员WC里面始終是存在活的線(xiàn)程的。
推薦使用 ThreadPoolExecutor 方式
在阿里的 Java 開(kāi)發(fā)手冊(cè)時(shí)有一條是不推薦使用 Executors 去創(chuàng)建,而是推薦去使用 ThreadPoolExecutor 來(lái)創(chuàng)建線(xiàn)程池。
這樣做的目的主要原因是:使用 Executors 創(chuàng)建線(xiàn)程池不會(huì)傳入核心參數(shù),而是采用的默認(rèn)值,這樣的話(huà)我們往往會(huì)忽略掉里面參數(shù)的含義,如果業(yè)務(wù)場(chǎng)景要求比較苛刻的話(huà),存在資源耗盡的風(fēng)險(xiǎn);另外采用 ThreadPoolExecutor 的方式可以讓我們更加清楚地了解線(xiàn)程池的運(yùn)行規(guī)則,不管是面試還是對(duì)技術(shù)成長(zhǎng)都有莫大的好處。
改了變量,其他線(xiàn)程可以立即知道。保證可見(jiàn)性的方法有以下幾種:
- volatile
加入 volatile 關(guān)鍵字的變量在進(jìn)行匯編時(shí)會(huì)多出一個(gè) lock 前綴指令,這個(gè)前綴指令相當(dāng)于一個(gè)內(nèi)存屏障,內(nèi)存屏障可以保證內(nèi)存操作的順序。當(dāng)聲明為 volatile 的變量進(jìn)行寫(xiě)操作時(shí),那么這個(gè)變量需要將數(shù)據(jù)寫(xiě)到主內(nèi)存中。
由于處理器會(huì)實(shí)現(xiàn)緩存一致性協(xié)議,所以寫(xiě)到主內(nèi)存后會(huì)導(dǎo)致其他處理器的緩存無(wú)效,也就是線(xiàn)程工作內(nèi)存無(wú)效,需要從主內(nèi)存中重新刷新數(shù)據(jù)。
網(wǎng)站題目:深入理解Java多線(xiàn)程核心知識(shí):跳槽面試必備技能
本文路徑:http://www.5511xx.com/article/dpjdesp.html


咨詢(xún)
建站咨詢(xún)
