并发编程之 锁的优化有哪些
前言
在 JDK 1.6 之前,synchronized 性能令人擔(dān)憂(yōu),但是 1.6 之后,JVM 團(tuán)隊(duì)針對(duì) synchronized 做了很多的優(yōu)化,讓 synchroized 在性能層面相比較 ReentrantLock 不相上下。那么,JVM 團(tuán)隊(duì)做了哪些優(yōu)化呢?
首先說(shuō),怎么才能優(yōu)化?我們知道,“鎖” 其實(shí)是互斥同步的具體實(shí)現(xiàn),而互斥同步對(duì)性能最大的影響是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要用戶(hù)態(tài)轉(zhuǎn)到內(nèi)核態(tài)來(lái)完成。這些操作給系統(tǒng)的并發(fā)性能帶來(lái)了很大的壓力。
所以,優(yōu)化的方向就是減少線程的阻塞,因?yàn)閽炱鹁€程和恢復(fù)線程需要切換到操作系統(tǒng)的內(nèi)核狀態(tài)。
Java 1.6 為了減少獲得鎖和釋放鎖帶來(lái)的性能損耗,引入了 “偏向鎖“ 和 ”輕量級(jí)鎖“ ,在 Java SE 1.6 中,鎖一共有4個(gè)狀態(tài),從低到高依次是:無(wú)鎖狀態(tài),偏向鎖狀態(tài),輕量級(jí)鎖狀態(tài),重量級(jí)鎖狀態(tài)。這幾個(gè)狀態(tài)會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)(即膨脹)。注意:鎖升級(jí)之后不能降級(jí)(具體原因后面講)。
1. 偏向鎖
虛擬機(jī)的團(tuán)隊(duì)根據(jù)經(jīng)驗(yàn)發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是有同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。
當(dāng)一個(gè)線程訪問(wèn)同步塊并獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行CAS操作來(lái)加鎖和解鎖,只需簡(jiǎn)單的測(cè)試一下對(duì)象頭的 “Mark Word” 里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。
如果測(cè)試成功,表示線程已經(jīng)獲得了鎖。如果測(cè)試失敗,則需要再測(cè)試一下 Mark Word 中偏向鎖的標(biāo)識(shí)是否設(shè)置了1(表示當(dāng)前還是偏向鎖):如果沒(méi)有設(shè)置,則使用CAS 競(jìng)爭(zhēng)鎖;如果設(shè)置了,則嘗試使用CAS 將對(duì)象頭的偏向鎖指向當(dāng)前線程。
可以說(shuō),偏向鎖的 “偏”,就是偏心的 “偏”,他的意思就是這個(gè)鎖會(huì)偏向于第一個(gè)獲得他的線程,如果在接下來(lái)的執(zhí)行過(guò)程中,該鎖沒(méi)有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要同步。
當(dāng)有另外要給線程去嘗試獲取這個(gè)鎖時(shí),偏向模式宣告結(jié)束,后續(xù)的操作將升級(jí)為輕量級(jí)鎖。
注意:偏向鎖可以提高有同步但無(wú)競(jìng)爭(zhēng)的程序性能,他同樣有缺陷:如果程序中大多數(shù)的鎖總是被多個(gè)不同的線程訪問(wèn),那偏向模式就是多余的。1.6之后的虛擬機(jī)默認(rèn)啟用偏向鎖,可以使用JVM參數(shù)來(lái)關(guān)閉:-XX:-UseBiasedLocking=false;程序?qū)⒛J(rèn)進(jìn)入輕量級(jí)鎖狀態(tài)。
可以看到,Mark Word 是實(shí)現(xiàn)偏向鎖的關(guān)鍵。而后面的輕量級(jí)鎖也是通過(guò)這個(gè)實(shí)現(xiàn)的。
2. 輕量級(jí)鎖
什么是輕量級(jí)鎖呢? “輕量級(jí)” 是相對(duì)于使用操作系統(tǒng)互斥量來(lái)實(shí)現(xiàn)的傳統(tǒng)鎖而言的,因此傳統(tǒng)的鎖機(jī)制稱(chēng)為 “重量級(jí)” 鎖。 首先需要強(qiáng)調(diào)一點(diǎn),輕量級(jí)鎖并不是用來(lái)代替重量級(jí)鎖的,他的本意是在沒(méi)有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能損耗。
線程在執(zhí)行同步塊之前,JVM 會(huì)先在當(dāng)前線程的棧幀中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭的 Mark Word 復(fù)制到鎖記錄中,官方稱(chēng)為 Displaced Mark Word. 然后線程嘗試使用CAS 將對(duì)象頭中的 Mark Word 替換為指向鎖記錄的指針。
如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競(jìng)爭(zhēng)鎖,當(dāng)前線程便會(huì)嘗試使用自旋來(lái)獲取鎖,注意:這里線程并沒(méi)有掛起自己,而是通過(guò)一定次數(shù)的自旋(默認(rèn)10次,可以使用 -XX:PreBlockSpin 修改),防止切換到內(nèi)核態(tài)導(dǎo)致的開(kāi)銷(xiāo)。
如果有2個(gè)以上的線程爭(zhēng)用同一把鎖,那么輕量級(jí)鎖將會(huì)失效,升級(jí)到重量級(jí)鎖。
那么為什么升級(jí)到重量級(jí)鎖之后不能降級(jí)呢?假設(shè)一下:如果鎖升級(jí)到重量級(jí)之后,拿到鎖的某個(gè)線程被阻塞了,等待了很久,那么輕量級(jí)線程將會(huì)一直自旋等待,消耗CPU性能。所以,在升級(jí)到重量級(jí)鎖后,就不能降級(jí)了,防止輕量級(jí)鎖自旋消耗CPU。
可以看到偏向鎖和輕量級(jí)鎖的差別,偏向鎖在第一個(gè)線程拿到鎖之后,將把線程ID 存儲(chǔ)在對(duì)象頭中,后面的所有操作都不是同步的,相當(dāng)于無(wú)鎖。而輕量級(jí)鎖,每次獲取鎖的時(shí)候還是需要使用CAS來(lái)修改對(duì)象頭的記錄,在沒(méi)有線程競(jìng)爭(zhēng)的情況下,這個(gè)操作是很輕量的,不需要使用操作系統(tǒng)的互斥機(jī)制。
3. 重量級(jí)鎖
相比較輕量級(jí)鎖是通過(guò)自旋來(lái)獲取鎖的,重量級(jí)鎖則是通過(guò)操作系統(tǒng)將線程切換到內(nèi)核態(tài)并阻塞來(lái)實(shí)現(xiàn)的。代價(jià)十分高昂。
下面看看各個(gè)鎖的優(yōu)缺點(diǎn)對(duì)比:
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距 | 如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷(xiāo)的消耗 | 適用于只有一個(gè)線程訪問(wèn)同步塊場(chǎng)景 |
| 輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了程序的響應(yīng)速度 | 如果始終得不到鎖競(jìng)爭(zhēng)的線程,使用自旋會(huì)消耗CPU | 追求響應(yīng)時(shí)間,同步塊執(zhí)行速度非常快 |
| 重量級(jí)鎖 | 線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗CPU | 線程阻塞,響應(yīng)時(shí)間緩慢 | 追求吞吐量,同步塊執(zhí)行時(shí)間較長(zhǎng) |
什么時(shí)候使用什么鎖,大家可以看看。
4. 鎖消除
什么是鎖消除呢?指的是 JIT 編譯器在運(yùn)行時(shí),對(duì)一些沒(méi)有必要同步的代碼卻同步了的鎖進(jìn)行消除。可以說(shuō)時(shí)一種徹底的鎖優(yōu)化。通過(guò)鎖消除,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間。
那么你們一定會(huì)問(wèn),誰(shuí)會(huì)這么傻,不需要同步還去同步啊?
請(qǐng)看下面的代碼:
public String[] createStrings(String[] args) {Vector<String> v = new Vector<>();for (int i = 0; i < 100; i++) {v.add(Integer.toString(i));}return v.toArray(new String[]{});}注意:v 變量只在這一個(gè)方法中使用,只是一個(gè)單純的局部變量,分配在棧中,也就沒(méi)有線程安全的說(shuō)法,任何同步都是沒(méi)有必要的,而Vector 的add 操作都是同步的。所以虛擬機(jī)檢測(cè)到這個(gè)情況,會(huì)將鎖去除。
鎖消除涉及一個(gè)技術(shù):逃逸分析。所謂逃逸分析就是觀察某一個(gè)變量十分會(huì)逃出某一個(gè)作用域。在本例中,變量v沒(méi)有逃出函數(shù)外,如果函數(shù)返回的不是 string 數(shù)組,而是 v 本身,那么就任務(wù) v 逃逸出了當(dāng)前函數(shù)。也就是說(shuō) v 可能被其他線程訪問(wèn)。如果是這樣,虛擬機(jī)就不能消除 v 的鎖操作。
5. 鎖粗化
原則上,我們?cè)诰帉?xiě)代碼的時(shí)候,總是推薦將同步塊盡可能的小。這樣是為了使得需要同步的操作數(shù)量小,如果存在鎖競(jìng)爭(zhēng),那等待鎖的線程也能盡快拿到鎖。
大部分情況下,這個(gè)原則是正確的。如果如果一系列連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒(méi)有線程競(jìng)爭(zhēng),頻繁的同步操作也會(huì)導(dǎo)致不必要的性能損耗。
如果虛擬機(jī)探測(cè)到很多零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部。即加大了同步塊。
6. 除了虛擬機(jī),程序員自己如何優(yōu)化鎖
鎖分離
減小鎖的持有時(shí)間。
其實(shí)這個(gè)很簡(jiǎn)單,你的鎖持有的時(shí)間長(zhǎng),后面的線程等待的時(shí)間就長(zhǎng),一個(gè)線程等待1秒,10000個(gè)線程就多等待了10000秒,因此,只在必要時(shí)進(jìn)行同步,這樣就能明顯減少線程持有鎖的時(shí)間。提高系統(tǒng)的吞吐量。
這個(gè)和我們上面說(shuō)的虛擬機(jī)幫助我們粗化時(shí)反的。但是,我們說(shuō),大部分情況下,減小鎖的粒度也削弱多線程競(jìng)爭(zhēng)的有效手段,比如 ConcurrentHashMap,他只鎖住了 Hash 桶中的某一個(gè)桶,不像HashTable 一樣鎖住整個(gè)對(duì)象。
我們之前在說(shuō) Java 世界的三把鎖的時(shí)候說(shuō)哪三把鎖,內(nèi)置鎖,重入鎖,讀寫(xiě)鎖,就是我們現(xiàn)在說(shuō)的讀寫(xiě)鎖 ReadWriteLock,使用讀寫(xiě)鎖來(lái)替代獨(dú)占鎖是減小鎖粒度的一種特殊情況,在讀多寫(xiě)少的場(chǎng)合,讀寫(xiě)鎖對(duì)系統(tǒng)性能是有好處的。可以有效提高系統(tǒng)的并發(fā)能力。因?yàn)樽x操作不會(huì)影響數(shù)據(jù)的完整性和一致性,就像 ConcurrentHashMap 的 get 方法一樣,根本不需要加鎖,這個(gè)時(shí)候又要說(shuō)說(shuō) HashTable ,該容器連 get 方法都加鎖。你可以想象一下。
如果將讀寫(xiě)鎖進(jìn)一步延伸,就是鎖分離,讀寫(xiě)鎖根據(jù)讀寫(xiě)操作功能的不同,進(jìn)行了有效的分離。而 JDK 的 LinkedBlockingQueue 則是鎖分離的最佳實(shí)踐。在進(jìn)行 take 操作和 put 操作使用了兩把不同的鎖。因?yàn)樗麄冎g根本沒(méi)有競(jìng)爭(zhēng)關(guān)系,或者說(shuō),使用隊(duì)列的數(shù)據(jù)結(jié)構(gòu),將原本耦合的業(yè)務(wù)分離了。
7. 總結(jié)
今天我們總結(jié)了一些鎖的優(yōu)化,有虛擬機(jī)的優(yōu)化,比如偏向鎖,輕量級(jí)鎖,自旋鎖,鎖粗化,鎖消除, 也有我們自己的優(yōu)化策略,需要平時(shí)寫(xiě)代碼的時(shí)候注意,比如減少鎖的持有時(shí)間,減小鎖的粒度,在讀多寫(xiě)少的場(chǎng)合使用讀寫(xiě)鎖,盡量通過(guò)合理的設(shè)計(jì)分離鎖。
總之,并發(fā)是門(mén)藝術(shù)。如何提高并發(fā)的性能是每個(gè)高級(jí)程序員的追求。
good luck !!!
轉(zhuǎn)載于:https://www.cnblogs.com/stateis0/p/9062003.html
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專(zhuān)家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的并发编程之 锁的优化有哪些的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: log 框架 之间的关系
- 下一篇: 配置maven nenux仓库