Java多線程優(yōu)化

一、Java 系統(tǒng)內(nèi)部鎖優(yōu)化

當(dāng)使用 Java 多線程訪問共享資源的時候,會出現(xiàn)競態(tài)的現(xiàn)象。即隨著時間的變化,多線程“寫”共享資源的最終結(jié)果會有所不同。為了解決這個問題,讓多線程“寫”資源的時候有先后順序,引入了鎖的概念。每次一個線程只能持有一個鎖進(jìn)行寫操作,其它線程等待該線程釋放鎖以后才能進(jìn)行后續(xù)操作。從這個角度來看,鎖的使用在 Java 多線程編程中是相當(dāng)重要的,那么是如何對鎖進(jìn)行優(yōu)化?Java 的鎖分為兩種:

一種是內(nèi)部鎖,它用 Synchronized 關(guān)鍵字來修飾,由 JVM 負(fù)責(zé)管理,并且不會出現(xiàn)鎖泄漏的情況。
另外一種是顯示鎖。

這里重點(diǎn)討論的是內(nèi)部鎖優(yōu)化。內(nèi)部鎖的優(yōu)化方式由 Java 內(nèi)部機(jī)制完成,雖然不需要程序員直接參與,但了解它對理解多線程優(yōu)化原理有很大幫助。這部分的優(yōu)化主要包括四部分:

  • 鎖消除
  • 鎖粗化
  • 偏向鎖
  • 適應(yīng)鎖

1??鎖消除(Lock Elision),JIT 編譯器對內(nèi)部鎖的優(yōu)化。在介紹其原理之前先說說,逃逸和逃逸分析。

逃逸是指在方法之內(nèi)創(chuàng)建的對象,除了在方法體之內(nèi)被引用之外,還在方法體之外被其它變量引用。也就是,在方法體之外引用方法內(nèi)的對象。在方法執(zhí)行完畢之后,方法中創(chuàng)建的對象應(yīng)該被 GC 回收,但由于該對象被其他變量引用,導(dǎo)致 GC無法回收。這個無法回收的對象稱為“逃逸”對象。Java 中的逃逸分析,就是對這種對象的分析。

回到鎖消除,Java JIT 會通過逃逸分析的方式,去分析加鎖的代碼段/共享資源,是否被一個或者多個線程使用,或者等待被使用。如果通過分析證實(shí),只被一個線程訪問,在編譯這個代碼段的時候就不生成 Synchronized 關(guān)鍵字,僅僅生成代碼對應(yīng)的機(jī)器碼。

換句話說,即便開發(fā)人員對代碼段/共享資源加上了 Synchronized(鎖),只要 JIT 發(fā)現(xiàn)這個代碼段/共享資源只被一個線程訪問,也會把這個 Synchronized(鎖)去掉。從而避免競態(tài),提高訪問資源的效率。

作為開發(fā)人員來說,只需要在代碼層面去考慮是否用 Synchronized(鎖)。說白了,就是感覺這段代碼有可能出現(xiàn)競態(tài),那么就使用 Synchronized(鎖),至于這個鎖是否真的會使用,則由 Java JIT 編譯器來決定。

2??鎖粗化(Lock Coarsening) ,是 JIT 編譯器對內(nèi)部鎖具體實(shí)現(xiàn)的優(yōu)化。假設(shè)有幾個在程序上相鄰的同步塊(代碼段/共享資源)上,每個同步塊使用的是同一個鎖實(shí)例。那么 JIT 會在編譯的時候?qū)⑦@些同步塊合并成一個大同步塊,并且使用同一個鎖實(shí)例。這樣避免一個線程反復(fù)申請/釋放鎖。

如圖三塊代碼段,分割成三個臨界區(qū),JIT 會將其合并為一個臨界區(qū),用一個鎖對其進(jìn)行訪問控制。即使在臨界區(qū)的空隙中,有其他的線程可以獲取鎖信息,JIT 編譯器執(zhí)行鎖粗化優(yōu)化的時候,會進(jìn)行命令重排到后一個同步塊的臨界區(qū)中。

鎖粗化默認(rèn)是開啟的。如果要關(guān)閉這個特性可以在 Java 程序的啟動命令行中添加虛擬機(jī)參數(shù)“-XX:-EliminateLocks”。

3??偏向鎖(Biased Locking),顧名思義,它會偏向于第一個訪問鎖的線程。如果在接下來的運(yùn)行中,該鎖沒有被其他線程訪問,則持有偏向鎖的線程不會觸發(fā)同步。相反,在運(yùn)行過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會被掛起,JVM 會消除掛起線程的偏向鎖。換句話說,偏向鎖只能在單個線程反復(fù)持有該鎖的時候起效。其目的是,為了避免相同線程獲取同一個鎖時,產(chǎn)生的線程切換,以及同步操作。

從實(shí)現(xiàn)機(jī)制上講, 每個偏向鎖都關(guān)聯(lián)一個計(jì)數(shù)器和一個占有線程。最開始沒有線程占有的時候,計(jì)數(shù)器為 0,鎖被認(rèn)為是 unheld 狀態(tài)。當(dāng)有線程請求 unheld 鎖時,JVM 記錄鎖的擁有者,并把鎖的請求計(jì)數(shù)加 1。如果同一線程再次請求鎖時,計(jì)數(shù)器就會增加 1,當(dāng)線程退出 Syncronized 時,計(jì)數(shù)器減 1,當(dāng)計(jì)數(shù)器為 0 時,鎖被釋放。

為了完成上述實(shí)現(xiàn),鎖對象中有個 ThreadId 字段。第一次獲取鎖之前,該字段是空的。持有鎖的線程,會將自身的 ThreadId 寫入到鎖的 ThreadId 中。下次有線程獲取鎖時,先檢查自身 ThreadId 是否和偏向鎖保存的 ThreadId 一致。如果一致,則認(rèn)為當(dāng)前線程已經(jīng)獲取了鎖,不需再次獲取鎖。偏向鎖默認(rèn)是開啟的。

如果要關(guān)閉這個特性,可以在 Java 程序的啟動命令行中添加虛擬機(jī)參數(shù)“-XX:-UseBiasedLocks”。

4??適應(yīng)鎖(Adaptive Locking):當(dāng)一個線程持申請鎖時,該鎖正在被其他線程持有。那么申請鎖的線程會進(jìn)入等待,等待的線程會被暫停,暫停的線程會產(chǎn)生上下文切換。由于上下文切換是比較消耗系統(tǒng)資源的,所以這種暫停線程的方式比較適合線程處理時間較長的情況。前面一個線程執(zhí)行的時間較長,才能彌補(bǔ)后面等待線程上下文切換的消耗。如果說線程執(zhí)行較短,那么也可以采取忙等(Busy Wait)的狀態(tài)。這種方式不會暫停線程,通過代碼中的 while 循環(huán)檢查鎖是否被釋放,一旦釋放就持有鎖的執(zhí)行權(quán)。

這種方式雖然不會帶來上下文的切換,但是會消耗 CPU 的資源。為了綜合較長和較短兩種線程等待模式,JVM 會根據(jù)運(yùn)行過程中收集到的信息來判斷,鎖持有時間是較長時間或者較短時間。然后再采取線程暫停或忙等的策略。

二、Java 代碼中如何進(jìn)行鎖優(yōu)化

鎖的開銷主要是在爭用鎖上,當(dāng)多線程對共享資源進(jìn)行訪問時,會出現(xiàn)線程等待。即便是使用內(nèi)存屏障,也會導(dǎo)致沖刷寫緩沖器,清空無效化隊(duì)列等開銷。

為了降低這種開銷,通常可以從幾個方面入手,例如:減少線程申請鎖的頻率(減少臨界區(qū))和減少線程持有鎖的時間長度(減小鎖顆粒)以及多線程的設(shè)計(jì)模式。

1??縮小臨界區(qū)的范圍

當(dāng)共享資源需要被多線程訪問時,會將共享資源或者代碼段放到臨界區(qū)中。如果在代碼書寫中減少臨界區(qū)的長度,就可以減少鎖被持有的時間,從而降低鎖被征用的概率,達(dá)到減少鎖開銷的目的。

如圖,盡量避免對一個方法進(jìn)行加鎖同步,可以只針對方法中的需要同步資源/變量進(jìn)行同步。其他的代碼段不放到 Synchronzied 中,減少臨界區(qū)的范圍。

2??減小鎖的顆粒度

減小鎖的顆粒度可以降低鎖的申請頻率,從而減小鎖被爭用的概率。其中一種常見的方法就是將一個顆粒度較粗的鎖拆分成顆粒度較細(xì)的鎖。

假設(shè)有一個類 ServerStatus,里面包含了四個方法:

  • addUser
  • addQuery
  • removeUser
  • removeQuery

如果分別在每個方法加上 Synchronized。在一個線程訪問其中任意一個方法的時候,將鎖住 ServerStatus,此時其他線程都無法訪問另外三個方法,從而進(jìn)入等待。

如果只針對每個方法內(nèi)部操作的對象加鎖,例如:addUser 和 removeUser 方法針對 users 對象加鎖;addQuery 和 removeQuery 方法針對 queries 對象加鎖。

假設(shè),當(dāng)一個線程池調(diào)用 addUser 方法的時候,只會鎖住 user 對象。另外一個線程是可以執(zhí)行 addQuery 和 removeQuery 方法的。

并不會因?yàn)殒i住整個對象而進(jìn)入等待。JDK 內(nèi)置的 ConcurrentHashMap 與 SynchronizedMap 就使用了類似的設(shè)計(jì)。

針對不同的方法中使用的對象進(jìn)行鎖定

3??讀寫鎖

也叫做線程的讀寫模式(Read-Write Lock),其本質(zhì)是一種多線程設(shè)計(jì)模式。將讀取操作和寫入操作分開考慮,在執(zhí)行讀取操作之前,線程必須獲取讀取的鎖。在執(zhí)行寫操作之前,必須獲取寫鎖。當(dāng)線程執(zhí)行讀取操作時,共享資源的狀態(tài)不會發(fā)生變化,其他的線程也可以讀取。但是在讀取時,不可以寫入。其實(shí),讀寫模式就是將原來共享資源的鎖,轉(zhuǎn)化成為讀和寫兩把鎖,將其分兩種情況考慮。

如果都是讀操作可以支持多線程同時進(jìn)行,只有在寫時其他線程才會進(jìn)入等待。

讀寫鎖類圖

說完了讀寫鎖的基本原理,再來看看參與的角色:

  1. Reader(讀者):對 SharedResource 角色執(zhí)行 Read 操作。
  2. Writer(寫者):對 SharedResource 角色執(zhí)行 Write 操作。
  3. SharedResource(共享資源):表示對 Reader 和 Writer 兩者共享的資源。
  4. ReadWriteLock(讀寫鎖),提供了 SharedResource 角色實(shí)現(xiàn) Read 操作和 Write 操作時所需的鎖。針對 Read 操作提供 readLock 和 readUnlock,對 Write 操作提供 writeLock 和 writeUnlock。

特別注意,在這里需要解決讀寫沖突的問題。

  1. 當(dāng)線程 A 獲取讀鎖時,如果有線程 B 正在執(zhí)行寫操作,線程 A 需要等待,否則會引起 read-write conflict(讀寫沖突)。
  2. 如果線程 B 正在執(zhí)行讀操作,線程 A 不需要等待,因?yàn)?read-read 不會引起conflict(沖突)。
  3. 當(dāng)線程 A 要獲取寫入鎖時,線程 B 正在執(zhí)行寫操作,線程 A 需要等待,否則會引起 write-write conflict(寫寫沖突)。
  4. 如果線程 B 正在執(zhí)行讀操作,則線程 A 需要等待,否則會引起 read-write conflict(讀寫沖突)。

三、線程池優(yōu)化

1??基本概念與原理

Java 線程池會生成一個隊(duì)列,要執(zhí)行的任務(wù)會被提交到這個隊(duì)列中。有一定數(shù)量的線程會在隊(duì)列中取任務(wù),然后執(zhí)行。任務(wù)執(zhí)行完畢以后,線程會返回任務(wù)隊(duì)列,等待其他任務(wù)并執(zhí)行。線程池中有一定數(shù)量的線程隨時待命。由于生成和維持這些線程是需要耗費(fèi)資源的,維持太多或者太少的線程都會對系統(tǒng)運(yùn)行效率造成影響,因此對線程池優(yōu)化是有意義的。

在做線程池調(diào)優(yōu)之前,先介紹一下線程的幾個基本參數(shù),以及線程池運(yùn)行的原理:

  1. 【corePoolSize】線程池的基本大小,無論是否有任務(wù)需要執(zhí)行,線程池中線程的個數(shù)。只有在工作隊(duì)列占滿的情況下,才會創(chuàng)建超出這個數(shù)量的線程。
  2. 【maximumPoolSize】線程池中允許存在的最大線程數(shù)。
  3. 【poolSize】線程池中線程的數(shù)量。

當(dāng)提交任務(wù)需要流程池處理時,會經(jīng)過以下判斷:

  1. 線程池中的線程數(shù)還沒有達(dá)到基本大小,也就是 poolSize<corePoolSize。
  2. 線程池中的線程數(shù)大于或等于基本大小,也就是 poolSize>=corePoolSize,且任務(wù)隊(duì)列未滿時,將任務(wù)提交到阻塞隊(duì)列排隊(duì)等候處理。
  3. 線程池中的線程數(shù)大于或等于基本大小,也就是 poolSize>=corePoolSize,且任務(wù)隊(duì)列占滿時,需要分兩種情況考慮:
    ①當(dāng) poolSize<maximumPoolSize,新增線程來處理任務(wù);
    ②當(dāng) poolSize=maximumPoolSize,線程池的處理能力達(dá)到極限,因此拒絕新增加的任務(wù)。

2??線程池容量配置

從上面線程池原理可以看出,corePoolSize 設(shè)置是整個線程池中最關(guān)鍵的參數(shù)。如果設(shè)置太小會導(dǎo)致線程池的吞吐量不足,因?yàn)樾绿峤坏娜蝿?wù)需要排隊(duì)或者被拒絕處理;設(shè)置太大可能會耗盡計(jì)算機(jī)的 CPU 和內(nèi)存資源。

那么如何配置合理的線程池大小呢?如果將被處理的任務(wù)分為,CPU 密集型任務(wù)和 IO 密集型任務(wù)。前者需要更多 CPU 的運(yùn)算操作,后者需要更多的 IO 操作。

CPU 密集型任務(wù)應(yīng)配置盡可能小的線程,如配置 CPU 個數(shù) +1 的線程數(shù)。IO 密集型任務(wù)應(yīng)配置盡可能多的線程,因?yàn)?IO 操作不占用 CPU,不要讓 CPU 閑下來,應(yīng)加大線程數(shù)量,如配置兩倍 CPU 個數(shù) +1。

CPU 的數(shù)字是一個假設(shè),實(shí)際環(huán)境中需要進(jìn)行測試,這里給大家一個思路。

若任務(wù)對其他系統(tǒng)資源有依賴,如任務(wù)依賴數(shù)據(jù)庫返回的結(jié)果(IO 操作)。其等待時間越長,CPU 空閑時間就越長,那么線程數(shù)量應(yīng)該越大,才能更好的利用 CPU。因此在 IO 優(yōu)化中發(fā)現(xiàn)一個估算公式:
最佳線程數(shù)目=((線程等待時間 + 線程CPU時間) / 線程CPU時間 ) * CPU數(shù)目
將公式進(jìn)一步化簡,得到:
最佳線程數(shù)目= (線程等待時間與線程CPU時間之比 + 1) * CPU數(shù)目

因此得到結(jié)論:線程等待時間所占比例越高,需要越多線程。線程 CPU 時間所占比例越高,需要越少線程。從另外一個角度驗(yàn)證上面對 IO 密集型(線程等待時間占比高)和 CPU 密集型(CPU 時間占比高)設(shè)置線程池大小的想法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,914評論 2 372