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