java并發編程中, 鎖機制對控制線程間共享內存的使用有重要的意義. 那么在Java內部鎖是如何實現的呢?
首先要明確一個概念.
Java中的鎖是對象級別的概念, 也就是每個對象都天生可以作為一個鎖使用.
究其底層實現, 實際上鎖是存在于Java對象頭的MarkWord
字段里的, 根據鎖的級別, 存儲結構不同, 但是都存在一個2bit的鎖標識位.
悲觀鎖是synchronize
內部的實現機制, java 1.6之后, 對悲觀鎖進行了改進, 也就是現在不是所有同步操作都一定會被阻塞. 如今的悲觀鎖按照鎖的獲取與釋放方式被劃分為偏向鎖, 輕量級鎖, 重量級鎖. 鎖標識位分別為01, 00, 10. 這幾種狀態會隨著鎖的競爭情況逐漸升級(但是不會降級)
偏向鎖是一種出現競爭才釋放鎖的機制.
具體來說, 作為鎖的對象頭部會保存一個偏向鎖標識
, 存儲當前獲得鎖的線程ID. 假設當前是線程A獲得了鎖, 但是線程A執行完同步塊之后并不釋放鎖, 而是等到其他線程競爭之后才進行鎖的釋放. 為什么要這樣設計呢? 因為如果下次還是線程A來訪問同一同步塊, 那么就無需通過CAS操作來重新對鎖對象的對象頭進行更新, 簡單比對一下線程id是否相等即可.
以下是偏向鎖的獲取與釋放流程圖:
對上圖的一點補充解釋:
在競爭偏向鎖的時候, 需要對持有偏向鎖的線程進行檢查, 如果線程已死, 則自動解鎖, 將鎖對象的對象頭的markword中的線程id置空, 如果線程當前已不使用同步塊, 解鎖, 將鎖對象的markword中額線程id置空, 并恢復線程. 同時其他線程可以通過CAS操作將鎖對象的對象頭markword中的對象id設置為自己的id, 也就是獲得了偏向鎖.
當偏向鎖的撤銷與獲取經常出現的時候(也就是鎖的競爭經常發生的時候), 就會升級為輕量級鎖.
線程通過CAS獲取輕量級鎖的方法與獲取偏向鎖的過程不同, 它總是將鎖對象的Markword先拷貝到棧空間, 然后試圖通過CAS更新Markword, 如果成功說明獲得了鎖, 如果失敗說明其他線程成功更新了Markword(也就是獲得了鎖), 則通過自旋CAS嘗試重新獲取鎖(始終消耗CPU時間片), 如果在較短的時間內成功, 則鎖不會升級, 如果長時間未獲得鎖, 則鎖會升級到重量級鎖.
重量級鎖在線程獲得鎖失敗時不會進行自旋CAS嘗試重新獲取鎖, 而是會進入阻塞態(讓出CPU時間片, 在其他線程釋放鎖后由CPU調度重新進行鎖競爭).
三種鎖并沒有好壞之分, 分別有各自適用的場合:
鎖 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 在沒有鎖競爭的情況下, 加鎖極快, 且無需解鎖 | 如果較多線程競爭鎖, 會使得加解鎖的過程額外費時 | 長時間一個線程訪問同步塊的場景 |
輕量級鎖 | 競爭線程不會阻塞, 響應快 | 如果長時間無法獲得鎖, 自旋CAS會帶來較大的CPU開銷 | 追求響應時間, 且同步塊執行速度塊的場景 |
重量級鎖 | 線程無法獲取鎖時總是阻塞, 及時讓出CPU資源 | 線程阻塞, 需要通過調度重新競爭鎖, 響應緩慢 | 追求高吞吐量, 且同步塊執行時間較長的場景, 如包含大量I/O操作 |
樂觀鎖允許所有的線程在不發生阻塞的情況下創建一份共享內存的拷貝. 這些線程接下來可能會對它們的拷貝進行修改,并企圖把它們修改后的版本通過CAS操作寫回到共享內存. 如果, 另一個線程已經修改了共享內存, 這個線程將不得不再次獲得一個新的拷貝, 在新的拷貝上做出修改, 并嘗試再次通過CAS把它們寫回到共享內存中去.
稱之為”樂觀鎖”的原因就是, 線程獲得它們想修改的數據的拷貝并做出修改, 樂觀的假設在此期間沒有其他線程對共享內存做出修改, 當這個樂觀假設成立時, 這個線程僅僅在無鎖的情況下完成共享內存的更新. 當這個假設不成立時, 線程所做的工作就會被丟棄.
無論樂觀假設成功或者失敗, 樂觀鎖總是沒有使用鎖進行并發的控制.
樂觀鎖僅適用于共享內存競用不是非常高的情況.
如果共享內存上的需要修改的內容非常多, 那么會因為CAS更新共享內存失敗, 從而浪費大量的CPU周期用在重新拷貝和修改上.
AtomicInteger
等.
新聞熱點
疑難解答