鎖策略
- 樂觀鎖
- 悲觀鎖
- 讀寫鎖
- 自旋鎖(輕量級鎖)
- 可重入鎖
- 死鎖
- CAS 介紹
- synchronized 背后的作業原理(重點)
- JUC 包的常見類
- ReentrantLock
- Semaphore
- 執行緒池
- 執行緒安全的集合類
樂觀鎖
一種抽象的感念,這個鎖認為出現鎖 競爭的概率比較低,(當前場景中,執行緒數目比較少,不太涉及競爭,就偶爾競爭一下),認為沖突的概率不是很高,做的作業會更少一些,付出的成本也更低,
悲觀鎖
這個鎖認為出現鎖 競爭的概率比較大,(當前場景中,執行緒的數目比較多,很可能涉及競爭),認為沖突的概率比較高,做的作業會更多一些,付出的成本也更高,
注:作業系統,提供的鎖介面,Mutex(互斥量),就是一個典型的悲觀鎖,認為競爭會很大,一旦出現了鎖競爭,就會讓競爭失敗的執行緒進行等待,什么時候被喚醒,就取決于調度器的實作,Mutex 也是重量級鎖,加鎖時遇到沖突,就會產生內核態和用戶態的切換,以及執行緒的大量阻塞和調度,開銷就會很大
synchronized 可以認為是悲觀鎖也可以認為是樂觀鎖,它是自適應的,它會根據具體的場景進行自我調節
讀寫鎖
多執行緒之間,資料的讀取方之間不會產生執行緒安全問題,但資料的寫入方互相之間以及和讀者之間都需要進行互斥,如果兩種場景下都用同一個鎖,就會產生極大的性能損耗,所以讀寫鎖因此而產生,讀寫鎖把讀操作和寫操作分開了,進一步降低了所沖突的概率,synchronized不是讀寫鎖,
自旋鎖(輕量級鎖)
自旋鎖又稱輕量級鎖,基于 CAS 實作可以實作自旋鎖,加鎖時遇到沖突,不會產生內核態和用戶態的切換,會一直回圈嘗試獲取鎖,不涉及執行緒調度,
可重入鎖
簡單理解就在對一個執行緒加了鎖后再加鎖而不是發生死鎖,就叫可重入鎖,synchronized 就是一個可重入鎖,如果對同一個執行緒加了兩次鎖,synchronized 內部持有當前是那個執行緒獲取的鎖,并維護了一個計數器,如果是同一個執行緒嘗試再次獲取鎖不會阻塞等待,而是單純計數器自增,如果釋放鎖,不是真的釋放,而是計數器自減,遇到計數器為 0 的時候才真正釋放鎖,
死鎖
死鎖是多執行緒開發中典型的問題,同時也是很嚴重的問題,一旦發生死鎖,程式就掛了,一個執行緒一把鎖不會發生死鎖,只有在多個執行緒多把鎖才可能發生死鎖(哲學家就餐問題),
多個執行緒同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放,由于執行緒被無限期地阻塞,因此程式不可能正常終止,所以死鎖產生的核心原因是環路等待,又稱回圈等待,
關于教科書中死鎖產生的四個必要條件:
- 互斥使用,即當資源被一個執行緒使用(占有)時,別的執行緒不能使用
- 不可搶占,資源請求者不能強制從資源占有者手中奪取資源,資源只能由資源占有者主動釋放,
- 請求和保持,即當資源請求者在請求其他的資源的同時保持對原有資源的占有,
- 回圈等待,即存在一個等待佇列:P1占有P2的資源,P2占有P3的資源,P3占有P1的資源,這樣就形成了一個等待環路,
個人認為回圈等待是最重要的因素,因為每一鎖都不能釋放自己所占有的資源,而其他三個原因更多的是指鎖的特性
解決死鎖
- 銀行家演算法,學過計算機專業的同學應該都聽說過,因為這個演算法在作業中不太會用到,所以以我目前的知識水平暫時不能具體介紹
- 不要在加鎖代碼中嘗試獲取其他鎖,這樣做就意味著在代碼里同一時刻只獲取一把鎖,
- 約定一定的順序來加鎖,
鎖策略小結

CAS 介紹
CAS: 全稱Compare and swap,字面意思:“比較并交換”,是一個原子性的操作,當多個執行緒同時對某個資源進行CAS操作,只能有一個執行緒操作成功,但是并不會阻塞其他執行緒,其他執行緒只會收到操作失敗的信號,可見 CAS 其實是一個樂觀鎖,
換句話說,CAS 就是原子的完成,從記憶體中進行比較,如果比較結果相同,就進行賦值、交換或其他操作
ABA 問題:例如,現在賬戶有1000元,執行緒1要進行轉賬50元時網路出現了問題卡了,于是用戶重新操作,第二個執行緒重新執行轉賬50成功,然后第一個執行緒又恢復,這時 CAS 原子操作就會和賬戶一開始讀取到的值對比(1000!=950),發現不相同,于是就不執行,轉賬結束符合預期,假如再次出現第三個執行緒,這個執行緒執行了入賬50元執行成功,并在一開始的第一個執行緒網路卡了的期間執行的,這時第一個執行緒的CAS原子操作和賬戶一開始的讀取到的值對比(1000=1000),就再次執行了轉賬,這樣就出現問題了,轉賬了兩次,進賬一次,不符合預期,這就是 ABA 問題,解決這個問題的方法就是在讀取的時候在添加一個讀取時間的引數也作為對比(也可以是版本號),
synchronized 背后的作業原理(重點)
JVM 將 synchronized 鎖分為 無鎖、偏向鎖、輕量級鎖、重量級鎖 狀態,會根據情況,進行依次升級,
圖片來源:

無鎖:沒有對資源進行鎖定,所有的執行緒都能訪問并修改同一個資源,但同時只有一個執行緒能修改成功,其他修改失敗的執行緒會不斷重試直到修改成功,
偏向鎖:物件的代碼一直被同一執行緒執行,不存在多個執行緒競爭,該執行緒在后續的執行中自動獲取鎖,降低獲取鎖帶來的性能開銷,偏向鎖,指的就是偏向第一個加鎖執行緒,該執行緒是不會主動釋放偏向鎖的,只有當其他執行緒嘗試競爭偏向鎖才會被釋放,
偏向鎖的撤銷,需要在某個時間點上沒有位元組碼正在執行時,先暫停擁有偏向鎖的執行緒,然后判斷鎖物件是否處于被鎖定狀態,如果執行緒不處于活動狀態,則將物件頭設定成無鎖狀態,并撤銷偏向鎖;如果執行緒處于活動狀態,升級為輕量級鎖的狀態,
輕量級鎖:輕量級鎖是指當鎖是偏向鎖的時候,被第二個執行緒 B 所訪問,此時偏向鎖就會升級為輕量級鎖,執行緒 B 會通過自旋的形式嘗試獲取鎖,執行緒不會阻塞,從而提高性能,當前只有一個等待執行緒,則該執行緒將通過自旋進行等待,但是當自旋超過一定的次數時,輕量級鎖便會升級為重量級鎖;當一個執行緒已持有鎖,另一個執行緒在自旋,而此時又有第三個執行緒來訪時,輕量級鎖也會升級為重量級鎖,
重量級鎖:指當有一個執行緒獲取鎖之后,其余所有等待獲取該鎖的執行緒都會處于阻塞狀態,重量級鎖通過物件內部的監視器(monitor)實作,而其中 monitor 的本質是依賴于底層作業系統的 Mutex Lock 實作,作業系統實作執行緒之間的切換需要從用戶態切換到內核態,切換成本非常高,
JUC 包的常見類
ReentrantLock
ReentrantLock 是可重入互斥鎖,這個把加鎖 lock() 和解鎖 unlock() 分開了,synchronized 是進入代碼塊加鎖出代碼塊解鎖,
所以:這就是為什么有了 synchronized 有時候還要用 ReentrantLock ,就因為有一個tryLock() 這個方法,
Semaphore
一個計數信號量,主要用于控制多執行緒對共同資源庫訪問的限制,類似于停車場顯示實時車位的那個
執行緒池
執行緒池主要是為了解決“頻繁創建和銷毀執行緒的開銷比較大”而引入的,把需要的執行緒提前準備好,放到池子里,需要用執行緒就從池子里取,而不是從系統中申請(從池子里去是純用戶態代碼,從系統中申請涉及到用戶態和內核態的切換,以及內核中的一些操作),
執行緒池是一個"過渡方案",現在有更優化的解決方案“協程”(Go,Python,更廣泛的使用協程)
使用Executors創建執行緒池,比較簡單,種類也比較優先,
使用ThreadPool創建執行緒池,比較復雜,種類更多,一般實際開發還是更鼓勵使用ThreadPool,更豐富的完成執行緒池的創建~
執行緒安全的集合類
ArrayList, LinkedList,Queue, HashMap, TreeMap, HashSet, TreeSet 都不是執行緒安全的!!!
1.多執行緒環境下使用順序表
a). 自己加鎖,
b). Collections.synchronizedList 相當于在ArrayList 等集合類上套了一層殼,殼里使用synchronized來加鎖,
c). CopyOnWriteArrayList 解決方案是讓不同的執行緒,使用不同的變數(沒加鎖),
⒉.多執行緒環境下使用佇列
BlockingQueue(一頭進,一頭出)
BlockDeque (兩頭進,兩頭出)
都是介面
3.多執行緒環境下使用哈希表
這里的面試常見問題是HashMap 和 HashTable 和 ConcurrentHashMap 區別?
HashTable, (不推薦使用),單純使用一個synchronized 進行加鎖,是針對了整個 HashTable 物件,壞處就是鎖沖突的概率是非常高的,而 HashMap 執行緒不安全,所以推薦使用 ConcurrentHashMap
ConcurrentHashMap 內部針對多執行緒做出了一定的優化[推薦使用],它不是針對整個物件加一把鎖,而是分成了很多把鎖,每個鏈表/紅黑樹分配一把鎖,(jdk1.7是分段鎖)只有當兩個執行緒恰好修改的是同一個鏈表/紅黑樹的時候才會涉及到鎖沖突~
ConcurrentHashMap 內部廣泛的使用了CAS操作,來提高效率,比如,獲取元素個數的時候,沒加鎖,直接CAS,比如修改元素,獲取對應的鏈表的下標的時候,也是用CAS,
ConcurrentHashMap 針對擴容進行了優化,Hash表的擴容是個麻煩事,需要把整個hash表拷貝一份,
如果是HashTable,某個執行緒t正好觸發了擴容,這個t就倒霉了,就需要負責完成整個擴容程序
如果是ConcurrentHashMap,把擴容任務分散開了,類似于"螞蟻搬家",就能夠更平滑的進行過度,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/295163.html
標籤:java
下一篇:jmap的使用以及記憶體溢位分析

