java裡的鎖總結(synchronized隱式鎖、Lock顯式鎖、volatile、CAS)
阿新 • • 發佈:2020-09-17
# 一、介紹
首先, java 的鎖分為兩類: 1. 第一類是 **synchronized** 同步關鍵字,這個關鍵字屬於隱式的鎖,是 jvm 層面實現,使用的時候看不見; 2. 第二類是在 jdk5 後增加的 **Lock** 介面以及對應的各種實現類,這屬於顯式的鎖,就是我們能在程式碼層面看到鎖這個物件,而這些個物件的方法實現,大都是直接依賴 CPU 指令的,無關 jvm 的實現。 接下來就從 **synchronized** 和 **Lock** 兩方面來講。
# 二、synchronized
## 2.1 synchronized 的使用
* 如果修飾的是`具體物件`:鎖的是`物件`; * 如果修飾的是`成員方法`:那鎖的就是 `this` ; * 如果修飾的是`靜態方法`:鎖的就是這個`物件.class`。 ## 2.2 Java的物件頭和 Monitor
關於操作 monitor 的具體實現,我們沒有再深入,持有管程、計數、阻塞等等的思路和直接在 java 中顯式的用 lock 是類似的。 早期的 synchronized 的實現就是基於上面所講的原理,因為監視器鎖(monitor)是**依賴於底層的作業系統的 Mutex Lock 來實現的**,而**作業系統**實現執行緒之間的切換時需要**從使用者態轉換到核心態**,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的 synchronized 效率低的原因。 > 更具體一些的開銷,還涉及 java 的執行緒和作業系統核心執行緒的關係 前面講到物件頭裡儲存的內容的時候我們也留了線索,那就是 jdk6 之後多出來輕量級的鎖,來改進 synchronized 的實現。 我的理解,這個改進就是:**從加鎖到最後變成以前的那種重量級鎖的過程裡,新實現出狀態不同的鎖作為過渡。** ## 2.5 改進後的各種鎖
**偏向鎖->自旋鎖->輕量級鎖->重量級鎖**。按照這個順序,鎖的重量依次增加。 - **偏向鎖**。他的意思是這個鎖會偏向於第一個獲得它的執行緒,當這個執行緒再次請求鎖的時候不需要進行任何同步操作,從而提高效能。那麼處於偏向鎖模式的時候,物件頭的Mark Word 的結構會變為偏向鎖結構。 > 研究發現,在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,因此為了減少同一執行緒獲取鎖的代價而引入偏向鎖。那麼顯然,一旦另一個執行緒嘗試獲得這個鎖,那麼偏向模式就會結束。另一方面,如果程式的大多數鎖都是多個執行緒訪問,那麼偏向鎖就是多餘的。 - **輕量級鎖**。當偏向鎖的條件不滿足,亦即的確有多執行緒併發爭搶同一鎖物件時,但併發數不大時,優先使用輕量級鎖。一般只有兩個執行緒爭搶鎖標記時,優先使用輕量級鎖。 此時,物件頭的Mark Word 的結構會變為輕量級鎖結構。 > 輕量級鎖是和傳統的重量級鎖相比較的,傳統的鎖使用的是作業系統的互斥量,而輕量級鎖是虛擬機器基於 CAS 操作進行更新,嘗試比較並交換,根據情況決定要不要改為重量級鎖。(這個動態過程也就是自旋鎖的過程了) * **重量級鎖**。重量級鎖即為我們在上面探討的**具有完整Monitor功能的鎖**。 * **自旋鎖**。自旋鎖是一個過渡鎖,是從輕量級鎖到重量級鎖的過渡。也就是CAS。 > CAS,全稱為Compare-And-Swap,是一條CPU的原子指令,其作用是讓CPU比較後原子地更新某個位置的值,實現方式是基於硬體平臺的彙編指令,就是說CAS是靠硬體實現的,JVM 只是封裝了彙編呼叫,那些AtomicInteger類便是使用了這些封裝後的介面。 注意:Java中的各種鎖對程式設計師來說是透明的: 在建立鎖時,JVM 先建立最輕的鎖,若不滿足條件則將鎖逐次升級.。這四種鎖之間只能升級,不能降級。 ## 2.6 其他鎖的分類
上面說的鎖都是基於 synchronized 關鍵字,以及底層的實現涉及到的鎖的概念,還有一些別的角度的鎖分類: ### 按照鎖的特性分類: 1. **悲觀鎖**:獨佔鎖,會導致其他所有需要所的執行緒都掛起,等待持有所的執行緒釋放鎖,就是說它的看法比較悲觀,認為悲觀鎖認為對於同一個資料的併發操作,一定是會發生修改的。因此對於同一個資料的併發操作,悲觀鎖採取加鎖的形式。比如前面講過的,最傳統的 synchronized 修飾的底層實現,或者重量級鎖。(但是現在synchronized升級之後,已經不是單純的悲觀鎖了) 2. **樂觀鎖**:每次不是加鎖,而是假設沒有衝突而去試探性的完成操作,如果因為衝突失敗了就重試,直到成功。比如 CAS 自旋鎖的操作,實際上並沒有加鎖。 ### 按照鎖的順序分類: 1. **公平鎖**。公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖。java 裡面可以通過 ReentrantLock 這個鎖物件,然後指定是否公平 2. **非公平鎖**。非公平鎖是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖。使用 synchronized 是無法指定公平與否的,他是不公平的。 ### 獨佔鎖(也叫排他鎖)/共享鎖: 1. **獨佔鎖也叫排他鎖**,是指該鎖一次只能被一個執行緒所持有。對 ReentrantLock 和 Sychronized 而言都是獨佔鎖。 2. **共享鎖**:是指該鎖可被多個執行緒所持有。對 ReentrantReadWriteLock 而言,其讀鎖是共享鎖,其寫鎖是獨佔鎖。讀鎖的共享性可保證併發讀是非常高效的,讀寫、寫讀、寫寫的過程都是互斥的。 獨佔鎖/共享鎖是一種廣義的說法,互斥鎖/讀寫鎖是java裡具體的實現。
# 三、Java 裡的 Lock
上面我們講到了,synchronized 關鍵字下層的鎖,是在 jvm 層面實現的,而後來在 jdk 5 之後,在 juc 包裡有了**顯式的鎖**,Lock 完全用 Java 寫成,在java這個層面是無關JVM實現的。雖然 Lock 缺少了 (通過 synchronized 塊或者方法所提供的) 隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種 synchronized 關鍵字所不具備的同步特性。 Lock 是一個介面,實現類常見的有: * 重入鎖(`ReentrantLock`) * 讀鎖(`ReadLock`) * 寫鎖(`WriteLock`) 實現基本都是通過**聚合**了一個同步器(`AbstractQueuedSynchronizer` 縮寫為 `AQS`)的子類來完成執行緒訪問控制的。 我們可以看看:
佇列同步器 AbstractQueuedSynchronizer(以下簡稱同步器或者 AQS),是用來構建鎖或者其他同步元件的基礎框架,它使用了一個 int 成員變量表示同步狀態,通過**內建的 FIFO 佇列**來完成資源獲取執行緒的排隊工作。 同步器的主要**使用方式是繼承**,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的 3 個方法來進行操作,因為它們能夠保證狀態的改變是安全的。 這三個方法分別是: 1. `protected final int getState()`,// 獲取當前同步狀態 2. `protected final void setState(int newState)`,// 設定當前同步狀態 3. `protected final boolean compareAndSetState(int expect, int update)`,// 使用 CAS 設定當前狀態,該方法能夠保證狀態設定的原子性 子類推薦被定義為自定義同步元件的靜態內部類,同步器自身沒有實現任何同步介面,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步元件使用,同步器既可以支援獨佔式地獲取同步狀態,也可以支援共享式地獲取同步狀態,這樣就可以方便實現不同型別的同步元件 (ReentrantLock、 ReentrantReadWriteLock 和 CountDownLatch 等)。 AQS 定義的三類模板方法; 1. **獨佔式**同步狀態獲取與釋放 2. **共享式**同步狀態獲取與釋放 3. 同步狀態和查詢**同步佇列**中的等待執行緒情況 同步器的內建 FIFO 佇列,從原始碼裡可以看到,Node 就是**儲存著執行緒引用和執行緒狀態的容器**。 * 每個執行緒對同步器的訪問,都可以看做是佇列中的一個節點(Node)。 * 節點是構成同步佇列的基礎,同步器擁有首節點 (head) 和尾節點 (tail); * 沒有成功獲取同步狀態的執行緒將會成為節點加入該佇列的尾部。 * 首節點的執行緒在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設定為首節點。 因為原始碼很多,這裡暫且不去分析具體的實現。 ## 3.2 重入鎖 ReentrantLock
* 重入鎖 ReentrantLock,就是支援重進入的鎖,它表示該鎖能夠支援一個執行緒對資源的重複加鎖。 * 除此之外,該鎖的還支援獲取鎖時的公平和非公平性選擇。 ReentrantLock 支援公平與非公平選擇,內部實現機制為: 1. 內部基於 `AQS` 實現一個公平與非公平公共的父類 `Sync` ,(在程式碼裡,Sync 是一個內部類,繼承 AQS)用於管理同步狀態; 2. `FairSync` 繼承 `Sync` 用於處理公平問題; 3. `NonfairSync` 繼承 `Sync` 用於處理非公平問題。 ## 3.3 讀寫鎖 ReentrantReadWriteLock
在上面講 synchronized 的最後,提到了鎖的其他維度的分類: 獨佔鎖(排他鎖)/共享鎖,具體實現層面就對應 java 裡的**互斥鎖/讀寫鎖**。 - ReentrantLock、synchronized 都是排他鎖; - ReentrantReadWriteLock 裡面維護了一個讀鎖、一個寫鎖,其中讀鎖是共享鎖,寫鎖是排他鎖。
# 四、一些總結和對比
到這裡我們知道了 java 的物件都有與之關聯的一個鎖,這個鎖稱為監視器鎖或者內部鎖,通過關鍵字 `synchronized` 宣告來使用,實際是 jvm 層面實現的,向下則用到了 Monitor 類,再向下虛擬機器的指令則是和 CPU 打交道,插入記憶體屏障等等操作。 而 jdk 5 之後引入了`顯式的鎖`,以 `Lock` 介面為核心的各種實現類,他們完全由 java 實現邏輯,那麼實現類還要基於 `AQS` 這個佇列同步器,AQS 遮蔽了同步狀態管理、執行緒排隊與喚醒等底層操作,提供模板方法,聚合到 Lock 的實現類裡去實現。 這裡我們對比一下隱式和顯式鎖: 1. 隱式鎖基本沒有靈活性可言,因為 synchronized 控制的程式碼塊無法跨方法,修飾的範圍很窄;而顯示鎖則本身就是一個物件,可以充分發揮面向物件的靈活性,完全可以在一個方法裡獲得鎖,另一個方法裡釋放。 2. 隱式鎖簡單易用且不會導致記憶體洩漏;而顯式鎖的過程完全要程式設計師控制,容易導致鎖洩露; 3. 隱式鎖只是非公平鎖;顯示鎖支援公平/非公平鎖; 4. 隱式鎖無法限制等待時間、無法對鎖的資訊進行監控;顯示鎖提供了足夠多的方法來完成靈活的功能; 5. 一般來說,我們預設情況下使用隱式鎖,只在需要顯示鎖的特性的時候才選用顯式鎖。 對比完了 `synchronized` 和 `Lock` 兩個**鎖**。對於 java 的執行緒同步機制,往往還會提到的另外兩個內容就是 `volatile` 關鍵字和 `CAS` 操作以及對應的原子類。 因此這裡再提一下: * `volatile` 關鍵字常被稱為輕量級的 synchronized,實際上這兩個完全不是一個東西。我們知道了 synchronized 通過的是 jvm 層面的管程隱式的加了鎖。而 volatile 關鍵字則是另一個角度,jvm 也採用相應的手段,保證: * 被它修飾的變數的可見性:執行緒對變數進行修改後,要立刻寫回主記憶體; * 執行緒對變數讀取的時候,要從主記憶體讀,而不是快取; * 在它修飾變數上的操作禁止指令重排序。 * `CAS` 是一種 CPU 的指令,也不屬於加鎖,它通過假設沒有衝突而去試探性的完成操作,如果因為衝突失敗了就重試,直到成功。那麼實際上我們很少直接使用 CAS ,但是 java 裡提供了一些原子變數類,就是 juc 包裡面的各種Atomicxxx類,這些類的底層實現直接使用了 CAS 操作來保證使用這些型別的變數的時候,操作都是原子操作,當使用他們作為共享變數的時候,也就不存線上程安全問題了。 參考: - 《Java併發程式設計的藝術》 - [Java 併發程式設計-專欄文章目錄彙總](https://blog.csdn.net/xiaohulunb/article/details/10