新聞中心
【稿件】隨著業(yè)務(wù)量的增加,多線程處理成為家常便飯。于是,多線程優(yōu)化成了擺在我們面前的問題。Java 作為當(dāng)今主流的應(yīng)用開發(fā)語言,也會(huì)有同樣的問題。

創(chuàng)新互聯(lián)公司專注于西疇企業(yè)網(wǎng)站建設(shè),成都響應(yīng)式網(wǎng)站建設(shè)公司,商城開發(fā)。西疇網(wǎng)站建設(shè)公司,為西疇等地區(qū)提供建站服務(wù)。全流程按需求定制制作,專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,創(chuàng)新互聯(lián)公司專業(yè)和態(tài)度為您提供的服務(wù)
圖片來自 Pexels
今天,我們從 Java 內(nèi)部鎖優(yōu)化,代碼中的鎖優(yōu)化,以及線程池優(yōu)化幾個(gè)方面展開討論。
Java 內(nèi)部鎖優(yōu)化
當(dāng)使用 Java 多線程訪問共享資源的時(shí)候,會(huì)出現(xiàn)競(jìng)態(tài)的現(xiàn)象。即隨著時(shí)間的變化,多線程“寫”共享資源的最終結(jié)果會(huì)有所不同。
為了解決這個(gè)問題,讓多線程“寫”資源的時(shí)候有先后順序,引入了鎖的概念。每次一個(gè)線程只能持有一個(gè)鎖進(jìn)行寫操作,其他的線程等待該線程釋放鎖以后才能進(jìn)行后續(xù)操作。
從這個(gè)角度來看,鎖的使用在 Java 多線程編程中是相當(dāng)重要的,那么是如何對(duì)鎖進(jìn)行優(yōu)化?
眾所周知,Java 的鎖分為兩種:
- 一種是內(nèi)部鎖,它用 Synchronized 關(guān)鍵字來修飾,由 JVM 負(fù)責(zé)管理,并且不會(huì)出現(xiàn)鎖泄漏的情況。
- 另外一種是顯示鎖。
這里重點(diǎn)討論的是內(nèi)部鎖優(yōu)化。內(nèi)部鎖的優(yōu)化方式由 Java 內(nèi)部機(jī)制完成,雖然不需要程序員直接參與,但了解它對(duì)理解多線程優(yōu)化原理有很大幫助。
這部分的優(yōu)化主要包括四部分:
- 鎖消除
- 鎖粗化
- 偏向鎖
- 適應(yīng)鎖
鎖消除(Lock Elision),JIT 編譯器對(duì)內(nèi)部鎖的優(yōu)化。在介紹其原理之前先說說,逃逸和逃逸分析。
逃逸是指在方法之內(nèi)創(chuàng)建的對(duì)象,除了在方法體之內(nèi)被引用之外,還在方法體之外被其他變量引用。
也就是,在方法體之外引用方法內(nèi)的對(duì)象。在方法執(zhí)行完畢之后,方法中創(chuàng)建的對(duì)象應(yīng)該被 GC 回收,但由于該對(duì)象被其他變量引用,導(dǎo)致 GC 無法回收。
這個(gè)無法回收的對(duì)象稱為“逃逸”對(duì)象。Java 中的逃逸分析,就是對(duì)這種對(duì)象的分析。
回到鎖消除,Java JIT 會(huì)通過逃逸分析的方式,去分析加鎖的代碼段/共享資源,他們是否被一個(gè)或者多個(gè)線程使用,或者等待被使用。
如果通過分析證實(shí),只被一個(gè)線程訪問,在編譯這個(gè)代碼段的時(shí)候就不生成 Synchronized 關(guān)鍵字,僅僅生成代碼對(duì)應(yīng)的機(jī)器碼。
換句話說,即便開發(fā)人員對(duì)代碼段/共享資源加上了 Synchronized(鎖),只要 JIT 發(fā)現(xiàn)這個(gè)代碼段/共享資源只被一個(gè)線程訪問,也會(huì)把這個(gè) Synchronized(鎖)去掉。從而避免競(jìng)態(tài),提高訪問資源的效率。
鎖消除示意圖
作為開發(fā)人員來說,只需要在代碼層面去考慮是否用 Synchronized(鎖)。
說白了,就是感覺這段代碼有可能出現(xiàn)競(jìng)態(tài),那么就使用 Synchronized(鎖),至于這個(gè)鎖是否真的會(huì)使用,則由 Java JIT 編譯器來決定。
鎖粗化(Lock Coarsening) ,是 JIT 編譯器對(duì)內(nèi)部鎖具體實(shí)現(xiàn)的優(yōu)化。假設(shè)有幾個(gè)在程序上相鄰的同步塊(代碼段/共享資源)上,每個(gè)同步塊使用的是同一個(gè)鎖實(shí)例。
那么 JIT 會(huì)在編譯的時(shí)候?qū)⑦@些同步塊合并成一個(gè)大同步塊,并且使用同一個(gè)鎖實(shí)例。這樣避免一個(gè)線程反復(fù)申請(qǐng)/釋放鎖。
鎖粗化示意圖
如上圖存在三塊代碼段,分割成三個(gè)臨界區(qū),JIT 會(huì)將其合并為一個(gè)臨界區(qū),用一個(gè)鎖對(duì)其進(jìn)行訪問控制。
即使在臨界區(qū)的空隙中,有其他的線程可以獲取鎖信息,JIT 編譯器執(zhí)行鎖粗化優(yōu)化的時(shí)候,會(huì)進(jìn)行命令重排到后一個(gè)同步塊的臨界區(qū)中。
鎖粗化默認(rèn)是開啟的。如果要關(guān)閉這個(gè)特性可以在 Java 程序的啟動(dòng)命令行中添加虛擬機(jī)參數(shù)“-XX:-EliminateLocks”。
偏向鎖(Biased Locking),顧名思義,它會(huì)偏向于第一個(gè)訪問鎖的線程。如果在接下來的運(yùn)行中,該鎖沒有被其他線程訪問,則持有偏向鎖的線程不會(huì)觸發(fā)同步。
相反,在運(yùn)行過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會(huì)被掛起,JVM 會(huì)消除掛起線程的偏向鎖。
換句話說,偏向鎖只能在單個(gè)線程反復(fù)持有該鎖的時(shí)候起效。其目的是,為了避免相同線程獲取同一個(gè)鎖時(shí),產(chǎn)生的線程切換,以及同步操作。
從實(shí)現(xiàn)機(jī)制上講, 每個(gè)偏向鎖都關(guān)聯(lián)一個(gè)計(jì)數(shù)器和一個(gè)占有線程。最開始沒有線程占有的時(shí)候,計(jì)數(shù)器為 0,鎖被認(rèn)為是 unheld 狀態(tài)。
當(dāng)有線程請(qǐng)求 unheld 鎖時(shí),JVM 記錄鎖的擁有者,并把鎖的請(qǐng)求計(jì)數(shù)加 1。
如果同一線程再次請(qǐng)求鎖時(shí),計(jì)數(shù)器就會(huì)增加 1,當(dāng)線程退出 Syncronized 時(shí),計(jì)數(shù)器減 1,當(dāng)計(jì)數(shù)器為 0 時(shí),鎖被釋放。
為了完成上述實(shí)現(xiàn),鎖對(duì)象中有個(gè) ThreadId 字段。第一次獲取鎖之前,該字段是空的。持有鎖的線程,會(huì)將自身的 ThreadId 寫入到鎖的 ThreadId 中。
下次有線程獲取鎖時(shí),先檢查自身 ThreadId 是否和偏向鎖保存的 ThreadId 一致。
如果一致,則認(rèn)為當(dāng)前線程已經(jīng)獲取了鎖,不需再次獲取鎖。偏向鎖默認(rèn)是開啟的。
如果要關(guān)閉這個(gè)特性,可以在 Java 程序的啟動(dòng)命令行中添加虛擬機(jī)參數(shù)“-XX:-UseBiasedLocks”。
適應(yīng)鎖(Adaptive Locking):當(dāng)一個(gè)線程持申請(qǐng)鎖時(shí),該鎖正在被其他線程持有。
那么申請(qǐng)鎖的線程會(huì)進(jìn)入等待,等待的線程會(huì)被暫停,暫停的線程會(huì)產(chǎn)生上下文切換。
由于上下文切換是比較消耗系統(tǒng)資源的,所以這種暫停線程的方式比較適合線程處理時(shí)間較長(zhǎng)的情況。
前面一個(gè)線程執(zhí)行的時(shí)間較長(zhǎng),才能彌補(bǔ)后面等待線程上下文切換的消耗。如果說線程執(zhí)行較短,那么也可以采取忙等(Busy Wait)的狀態(tài)。
這種方式不會(huì)暫停線程,通過代碼中的 while 循環(huán)檢查鎖是否被釋放,一旦釋放就持有鎖的執(zhí)行權(quán)。
這種方式雖然不會(huì)帶來上下文的切換,但是會(huì)消耗 CPU 的資源。為了綜合較長(zhǎng)和較短兩種線程等待模式,JVM 會(huì)根據(jù)運(yùn)行過程中收集到的信息來判斷,鎖持有時(shí)間是較長(zhǎng)時(shí)間或者較短時(shí)間。然后再采取線程暫?;蛎Φ鹊牟呗?。
Java 代碼中如何進(jìn)行鎖優(yōu)化
前面講了 Java 系統(tǒng)是如何針對(duì)內(nèi)部鎖進(jìn)行優(yōu)化的。如果說內(nèi)部鎖的優(yōu)化是 Java 系統(tǒng)自身完成的話,那么接下來的優(yōu)化就需要通過代碼實(shí)現(xiàn)了。
鎖的開銷主要是在爭(zhēng)用鎖上,當(dāng)多線程對(duì)共享資源進(jìn)行訪問時(shí),會(huì)出現(xiàn)線程等待。
即便是使用內(nèi)存屏障,也會(huì)導(dǎo)致沖刷寫緩沖器,清空無效化隊(duì)列等開銷。
為了降低這種開銷,通??梢詮膸讉€(gè)方面入手,例如:減少線程申請(qǐng)鎖的頻率(減少臨界區(qū))和減少線程持有鎖的時(shí)間長(zhǎng)度(減小鎖顆粒)以及多線程的設(shè)計(jì)模式。
減少臨界區(qū)的范圍
當(dāng)共享資源需要被多線程訪問時(shí),會(huì)將共享資源或者代碼段放到臨界區(qū)中。
如果在代碼書寫中減少臨界區(qū)的長(zhǎng)度,就可以減少鎖被持有的時(shí)間,從而降低鎖被征用的概率,達(dá)到減少鎖開銷的目的。
減少臨界區(qū)示例圖
如上圖,盡量避免對(duì)一個(gè)方法進(jìn)行加鎖同步,可以只針對(duì)方法中的需要同步資源/變量進(jìn)行同步。其他的代碼段不放到 Synchronzied 中,減少臨界區(qū)的范圍。
減小鎖的顆粒度
減小鎖的顆粒度可以降低鎖的申請(qǐng)頻率,從而減小鎖被爭(zhēng)用的概率。其中一種常見的方法就是將一個(gè)顆粒度較粗的鎖拆分成顆粒度較細(xì)的鎖。
拆分鎖的顆粒度
假設(shè)有一個(gè)類 ServerStatus,里面包含了四個(gè)方法:
- addUser
- addQuery
- removeUser
- removeQuery
如果分別在每個(gè)方法加上 Synchronized。在一個(gè)線程訪問其中任意一個(gè)方法的時(shí)候,將鎖住 ServerStatus,此時(shí)其他線程都無法訪問另外三個(gè)方法,從而進(jìn)入等待。
如果只針對(duì)每個(gè)方法內(nèi)部操作的對(duì)象加鎖,例如:addUser 和 removeUser 方法針對(duì) users 對(duì)象加鎖。又例如:addQuery 和 removeQuery 方法針對(duì) queries 對(duì)象加鎖。
假設(shè),當(dāng)一個(gè)線程池調(diào)用 addUser 方法的時(shí)候,只會(huì)鎖住 user 對(duì)象。另外一個(gè)線程是可以執(zhí)行 addQuery 和 removeQuery 方法的。
并不會(huì)因?yàn)殒i住整個(gè)對(duì)象而進(jìn)入等待。JDK 內(nèi)置的 ConcurrentHashMap 與 SynchronizedMap 就使用了類似的設(shè)計(jì)。
針對(duì)不同的方法中使用的對(duì)象進(jìn)行鎖定
讀寫鎖
也叫做線程的讀寫模式(Read-Write Lock),其本質(zhì)是一種多線程設(shè)計(jì)模式。
將讀取操作和寫入操作分開考慮,在執(zhí)行讀取操作之前,線程必須獲取讀取的鎖。
在執(zhí)行寫操作之前,必須獲取寫鎖。當(dāng)線程執(zhí)行讀取操作時(shí),共享資源的狀態(tài)不會(huì)發(fā)生變化,其他的線程也可以讀取。但是在讀取時(shí),不可以寫入。
其實(shí),讀寫模式就是將原來共享資源的鎖,轉(zhuǎn)化成為讀和寫兩把鎖,將其分兩種情況考慮。
如果都是讀操作可以支持多線程同時(shí)進(jìn)行,只有在寫時(shí)其他線程才會(huì)進(jìn)入等待。
Reader 線程正在讀取,Writer 線程正在等待
Writer 線程正在寫入,Reader 線程正在等待
讀寫鎖類圖
說完了讀寫鎖的基本原理,再來看看參與的角色:
- Reader(讀者),對(duì) SharedResource 角色執(zhí)行 Read 操作。
- Writer(寫者),對(duì) SharedResource 角色執(zhí)行 Write 操作。
- SharedResource(共享資源),表示對(duì) Reader 和 Writer 兩者共享的資源。
- ReadWriteLock(讀寫鎖),提供了 SharedResource 角色實(shí)現(xiàn) Read 操作和 Write 操作時(shí)所需的鎖。
針對(duì) Read 操作提供 readLock 和 readUnlock,對(duì) Write 操作提供 writeLock 和 writeUnlock。
特別需要注意的是,在這里需要解決讀寫沖突的問題。當(dāng)線程 A 獲取讀鎖時(shí),如果有線程 B 正在執(zhí)行寫操作,線程 A 需要等待,否則會(huì)引起 read-write conflict(讀寫沖突)。
如果線程 B 正在執(zhí)行讀操作,線程 A 不需要等待,因?yàn)?read-read 不會(huì)引起 conflict(沖突)。
當(dāng)線程 A 要獲取寫入鎖時(shí),線程 B 正在執(zhí)行寫操作,線程 A 需要等待,否則會(huì)引起 write-write conflict(寫寫沖突)。
如果線程 B 正在執(zhí)行讀操作,則線程 A 需要等待,否則會(huì)引起 read-write conflict(讀寫沖突)。
讀寫鎖沖突示例圖
上面基本把讀寫鎖的基本原理說完了,接下來通過一些代碼片段來看看它是如何實(shí)現(xiàn)的。
我們通過 Data 類 SharedResource,ReaderThread 和 WriterThread 來實(shí)現(xiàn) Reader 和 Writer,ReadWriteLock 類來實(shí)現(xiàn)讀寫鎖。
首先來看 ReaderThread 和 WriterThread,它們的實(shí)現(xiàn)相對(duì)簡(jiǎn)單。僅僅調(diào)用 Data 類中的 Read 和 Write 方法來實(shí)現(xiàn)讀寫操作。
ReaderThread 對(duì) Reader 的實(shí)現(xiàn)
WriterThread 對(duì) Writer 的實(shí)現(xiàn)
接下來就是 ReadWriteLock 類,它實(shí)現(xiàn)了讀寫鎖的具體功能。其中的幾個(gè)變量用來控制訪問線程和寫入優(yōu)先級(jí):
- readingReaders:正在讀取共享資源的線程個(gè)數(shù),整型。
- waitingWriters:正在等待寫入共享資源的線程個(gè)數(shù),整型。
- writingWriters:正在寫入共享資源的線程個(gè)數(shù),整型。
- preferWriter:寫入優(yōu)先級(jí)標(biāo)示,布爾型,為 true 表示寫入優(yōu)先;為 false 表示讀取優(yōu)先。
里面包含了四個(gè)方法,分別是:
- readLock
- readUnlock
- writeLock
- writeUnlock
顧名思義,分別對(duì)應(yīng)讀鎖定,讀解鎖,寫鎖定,寫解鎖的操作。兩兩組合以后一共四種方法。
ReadWriteLock 示例圖
在 ReadWriteLock 定義的四種方法中,各自完成不同的任務(wù):
- readLock,讀鎖。線程在讀的時(shí)候,檢查是否有寫線程在執(zhí)行,如果有就需要等待。同時(shí)還會(huì)觀察,在寫入優(yōu)先的時(shí)候,是否有等待寫入的線程。
如果存在也需要等待,等待寫入操作的線程完成再執(zhí)行。如果以上條件都沒有滿足,那么進(jìn)行讀操作,并將讀取線程數(shù) +1。
- readUnlock,讀解鎖。線程在讀操作完成以后,將讀取線程數(shù) -1。通知其他等待線程執(zhí)行。
- writeLock,寫鎖。先將寫等待線程數(shù) +1。如果發(fā)現(xiàn)有正在讀的線程或者有正寫的線程,那么進(jìn)入等待。否則,進(jìn)行寫操作,并將正在寫操作線程數(shù) +1。
- writeUnlock,寫解鎖。線程在寫操作完成以后,將寫線程數(shù) -1。通知其他等待線程執(zhí)行。
最后,來看共享資源的類:Data。它主要承載讀寫的方法。需要注意的是在做讀/寫的前后,需要加上對(duì)應(yīng)的鎖。
例如:在做讀操作(doRead)之前需要加上 readLock(讀鎖),在完成讀操作以后釋放讀鎖(readUnlock)。
又例如:在做寫操作(doWrite)之前需要加上 writeLock(寫鎖),在完成寫操作以后釋放寫鎖(writeUnlock)。
共享資源類 Data 示例圖
上面的幾個(gè)類已經(jīng)介紹完了,如果需要測(cè)試可以通過調(diào)用 ReaderThread 和 WriterThread 來完成調(diào)試。
讀寫鎖測(cè)試
線程池優(yōu)化
前面兩部分談到多線程對(duì)內(nèi)部鎖的優(yōu)化,以及代碼中對(duì)鎖的優(yōu)化。是從減少競(jìng)態(tài)的角度來優(yōu)化程序的。
如果從提高線程執(zhí)行效率,來對(duì)多線程程序進(jìn)行優(yōu)化,自然讓人聯(lián)想到了線程池技術(shù)。
基本概念與原理
Java 線程池會(huì)生成一個(gè)隊(duì)列,要執(zhí)行的任務(wù)會(huì)被提交到這個(gè)隊(duì)列中。有一定數(shù)量的線程會(huì)在隊(duì)列中取任務(wù),然后執(zhí)行。
任務(wù)執(zhí)行完畢以后,線程會(huì)返回任務(wù)隊(duì)列,等待其他任務(wù)并執(zhí)行。線程池中有一定數(shù)量的線程隨時(shí)待命。
由于生成和維持這些線程是需要耗費(fèi)資源了,維持太多或者太少的線程都會(huì)對(duì)系統(tǒng)運(yùn)行效率造成影響,因此對(duì)線程池優(yōu)化是有意義的。
在做線程池調(diào)優(yōu)之前,先介紹一下線程的幾個(gè)基本參數(shù),以及線程池運(yùn)行的原理:
- corePoolSize,線程池的基本大小,無論是否有任務(wù)需要執(zhí)行,線程池中線程的個(gè)數(shù)。只有在工作隊(duì)列占滿的情況下,才會(huì)創(chuàng)建超出這個(gè)數(shù)量的線程。
- maximumPoolSize,線程池中允許存在的最大線程數(shù)。
- poolSize,線程池中線程的數(shù)量。
當(dāng)提交任務(wù)需要流程池處理時(shí),會(huì)經(jīng)過以下判斷:
- 線程池中的線程數(shù)還沒有達(dá)到基本大小,也就是 poolSize
- 線程池中的線程數(shù)大于或等于基本大小,也就是 poolSize>=corePoolSize,并且任務(wù)隊(duì)列未滿時(shí),將任務(wù)提交到阻塞隊(duì)列排隊(duì)等候處理。
- 如果當(dāng)前線程池的線程數(shù)大于或等于基本大小,也就是 poolSize>=corePoolSize 且任務(wù)隊(duì)列占滿時(shí),需要分兩種情況考慮。
①當(dāng) poolSize 線程池容量配置 從上面線程池原理可以看出,corePoolSize 設(shè)置是整個(gè)線程池中最關(guān)鍵的參數(shù)。 如果設(shè)置太小會(huì)導(dǎo)致線程池的吞吐量不足,因?yàn)樾绿峤坏娜蝿?wù)需要排隊(duì)或者被拒絕處理;設(shè)置太大可能會(huì)耗盡計(jì)算機(jī)的 CPU 和內(nèi)存資源。 那么如何配置合理的線程池大小呢?如果將被處理的任務(wù)分為,CPU 密集型任務(wù)和 IO 密集型任務(wù)。前者需要更多 CPU 的運(yùn)算操作,后者需要更多的 IO 操作。 CPU 密集型任務(wù)應(yīng)配置盡可能小的線程,如配置 CPU 個(gè)數(shù) +1 的線程數(shù),IO 密集型任務(wù)應(yīng)配置盡可能多的線程,因?yàn)?IO 操作不占用 CPU,不要讓 CPU 閑下來,應(yīng)加大線程數(shù)量,如配置兩倍 CPU 個(gè)數(shù) +1。 CPU 的數(shù)字是一個(gè)假設(shè),實(shí)際環(huán)境中需要進(jìn)行測(cè)試,這里給大家一個(gè)思路。 若任務(wù)對(duì)其他系統(tǒng)資源有依賴,如任務(wù)依賴數(shù)據(jù)庫(kù)返回的結(jié)果(IO 操作)。其等待時(shí)間越長(zhǎng),CPU 空閑時(shí)間就越長(zhǎng),那么線程數(shù)量應(yīng)該越大,才能更好的利用 CPU。 因此在 IO 優(yōu)化中發(fā)現(xiàn)一個(gè)估算公式: 最佳線程數(shù)目=((線程等待時(shí)間+線程 CPU 時(shí)間)/線程 CPU 時(shí)間 )* CPU 數(shù)目。 將公式進(jìn)一步化簡(jiǎn),得到: 最佳線程數(shù)目= (線程等待時(shí)間與線程 CPU 時(shí)間之比+1)* CPU 數(shù)目。 因此得到結(jié)論:線程等待時(shí)間所占比例越高,需要越多線程。線程 CPU 時(shí)間所占比例越高,需要越少線程。 從另外一個(gè)角度驗(yàn)證上面對(duì) IO 密集型(線程等待時(shí)間占比高)和 CPU 密集型(CPU 時(shí)間占比高)設(shè)置線程池大小的想法。 總結(jié) Java 多線程開發(fā)優(yōu)化有兩個(gè)思路: 我們從內(nèi)部鎖優(yōu)化原理入手,分別介紹了鎖消除,鎖粗化,偏向鎖,適應(yīng)鎖,都是以 Java 系統(tǒng)本身來做優(yōu)化的,作為程序員需要了解其實(shí)現(xiàn)原理。 針對(duì) Java 代碼中鎖的優(yōu)化,我們又提出了,減少臨界區(qū)范圍,減小鎖的顆粒度,讀寫鎖(設(shè)計(jì)模式)等方法。 其中,讀寫鎖只是多線程設(shè)計(jì)模式中的一種,如果有興趣可以擴(kuò)展閱讀其他的設(shè)計(jì)模式,協(xié)助進(jìn)行多線程開發(fā)。最后針對(duì)線程池實(shí)現(xiàn)原理,提出了設(shè)置線程池大小的思路。 作者:崔皓 簡(jiǎn)介:十六年開發(fā)和架構(gòu)經(jīng)驗(yàn),曾擔(dān)任過惠普武漢交付中心技術(shù)專家,需求分析師,項(xiàng)目經(jīng)理,后在創(chuàng)業(yè)公司擔(dān)任技術(shù)/產(chǎn)品經(jīng)理。善于學(xué)習(xí),樂于分享。目前專注于技術(shù)架構(gòu)與研發(fā)管理。 【原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為.com】
名稱欄目:Java多線程優(yōu)化都不會(huì),怎么拿Offer?
文章網(wǎng)址:http://www.dlmjj.cn/article/dheioho.html


咨詢
建站咨詢
