java1.6爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了"偏向鎖"和"輕量級鎖"。html
偏向鎖爲了解決大部分狀況下只有一個線程持有鎖的狀況。java
大概邏輯是:每次得到鎖時,在鎖的對象頭信息中存儲了當前線程的ID,下次獲取鎖的時候,不需首先使用CAS競爭鎖,只須要去對象頭裏查看是不是當前線程的ID便可。程序員
ps:其實對象頭中的線程ID應該時刻變化的,不知道細節是怎麼處理的。暫時就瞭解到這一步。再也不深刻學習。數組
輕量級鎖使用自旋的CAS獲取鎖。緩存
重量級鎖使用阻塞來獲取鎖。安全
在java中,實例域,靜態域,數組元素都是存放在堆內存中的。對內存在線程之間共享。app
局部變量,方法定義參數,異常處理器參數不會再線程之間共享。框架
線程之間的共享變量存儲在主內存中(Main Memory),每個線程都有本身的本地內存(Local Memory),本地內存中存儲着讀/寫共享變量的副本。ide
由上圖能夠看出,線程之間的通訊由兩個步驟:函數
線程A把修改後的本地內存中的共享變量更新到主內存中去
線程B到主內存中讀取線程A以前更新過的共享變量
從總體上看,這就是線程A在向線程B發送消息,並且這個消息必須通過主內存。JMM經過控制主內存與每一個線程的本地內存之間的交互,來爲Java程序員提供內存可見性保證。
重排序包含兩種:
一旦咱們使用volatile語義或者synchronize同步,或者鎖,都會限制重排序。
限制重排序是經過內存柵欄實現的。
內存柵欄不只能限制重排序,還能經過刷新每一個CPU的緩存實現可視性。
有四類內存柵欄指令:
屏障類型 |
指令示例 |
說明 |
LoadLoad Barriers |
Load1; LoadLoad; Load2 |
確保Load1數據的裝載,以前於Load2及全部後續裝載指令的裝載。 |
StoreStore Barriers |
Store1; StoreStore; Store2 |
確保Store1數據對其餘處理器可見(刷新到內存),以前於Store2及全部後續存儲指令的存儲。 |
LoadStore Barriers |
Load1; LoadStore; Store2 |
確保Load1數據裝載,以前於Store2及全部後續的存儲指令刷新到內存。 |
StoreLoad Barriers |
Store1; StoreLoad; Load2 |
確保Store1數據對其餘處理器變得可見(指刷新到內存),以前於Load2及全部後續裝載指令的裝載。StoreLoad Barriers會使該屏障以前的全部內存訪問指令(存儲和裝載指令)完成以後,才執行該屏障以後的內存訪問指令。 |
若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。
happens-before規則以下:
注:這裏只羅列了4個,其實還有,能夠參考上一篇文章。
注意,兩個操做之間具備happens-before關係,並不意味着前一個操做必需要在後一個操做以前執行(不用緊跟着執行)!happens-before僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前
無論怎麼重排序(編譯器和處理器爲了提升並行度),程序的執行結果不能被改變,編譯器,runtime和處理器都必須遵照as-if-serial語義。
爲了遵照as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。
數據依賴關係如:
a = 1;b=a;
或者:
a = 1;a=2;
或者:
a= b;b=1
這是一種理想化的理論參考模型。順序一致性內存模型進制重排序,每一個操做都必須原子執行並當即對全部線程可見。
volatile內衣保證原子性和可見性。
具體實現原理是:
使用內存柵欄,在代碼的相應位置插入相應的內存柵欄指令,能夠作到:
鎖的內存語義的實現:
個人理解:鎖是java使用代碼實現的,處理器指令層面沒有鎖的概念。
具體細節我就再也不描述了,大概邏輯是:
AbstractQueuedSynchronizer是一個底層的虛擬類,它內部定義了一個volatile的int屬性(由於是可重入的,這裏是一個計數)。經過對着屬性執行CAS操做實現相似於tryLock的操做。
若是是"輕量級鎖",那麼lock操做應該是自旋tryLock實現的。
若是是"重量級鎖",那麼lock操做應該是在tryLock失敗以後直接掛起,等待被喚醒後重試。(我理解的,掛起和喚醒是操做系統支持的語義)
由於AbstractQueuedSynchronizer比較特殊,在它的基礎上實現了ReentrantLock,ReadWriteLock,以及閉鎖,信號量等。因此這裏把它的API列出來。
AbstractQueuedSynchronizer是一個抽象類,它內部維護了一個volatile類型的int對象,叫state,同時實現了一個不可重寫的final方法compareAndSetState,這個方法提供對state進行原子的CAS操做(應該是native代碼編寫的)。還有兩個一樣final和原子的方法:getState和setState。
AbstractQueuedSynchronizer內部還維護了一個先進先出的隊列。
它的API說明以下:
爲實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖定和相關同步器(信號量、事件,等等)提供一個框架。
此類的設計目標是成爲依靠單個原子 int 值來表示狀態的大多數同步器的一個有用基礎。
子類必須定義更改此狀態的受保護方法,並定義哪一種狀態對於此對象意味着被獲取或被釋放。假定這些條件以後,此類中的其餘方法就能夠實現全部排隊(經過Queue)和阻塞(經過volatile和CAS)機制。
子類能夠維護其餘狀態字段,但只是爲了得到同步而只追蹤使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法來操做以原子方式更新的 int 值。
應該將此類定義爲非公共內部幫助器類,可用它們來實現其封閉類的同步屬性。類 AbstractQueuedSynchronizer 沒有實現任何同步接口。而是定義了諸如 acquireInterruptibly(int) 之類的一些方法,在適當的時候能夠經過具體的鎖定和相關同步器來調用它們,以實現其公共方法。
使用
爲了將此類用做同步器的基礎,須要適當地從新定義如下方法,這是經過使用 getState()、setState(int) 和/或 compareAndSetState(int, int) 方法來檢查和/或修改同步狀態來實現的:
即便此類基於內部的某個 FIFO 隊列,它也沒法強行實施 FIFO 獲取策略。獨佔同步的核心採用如下形式:
Acquire:
while (!tryAcquire(arg)) {
enqueue thread if it is not already queued;
possibly block current thread;
}
Release:
if (tryRelease(arg))
unblock the first queued thread;
用例
如下是一個非再進入的互斥鎖定類,它使用值 0 表示未鎖定狀態,使用 1 表示鎖定狀態。它還支持一些條件並公開了一個檢測方法:
如下是一個鎖存器類,它相似於 CountDownLatch,除了只須要觸發單個 signal 以外。由於鎖存器是非獨佔的,因此它使用 shared 的獲取和釋放方法。
接下來將分析ReentrantReadWriteLock的實現,主要包括:讀寫狀態的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級(如下沒有特別說明讀寫鎖都可認爲是ReentrantReadWriteLock)。
讀寫鎖一樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個線程重複獲取的次數,而讀寫鎖的自定義同步器須要在同步狀態(一個整型變量)上維護多個讀線程和一個寫線程的狀態,使得該狀態的設計成爲讀寫鎖實現的關鍵。
若是在一個整型變量上維護多種狀態,就必定須要"按位切割使用"這個變量,讀寫鎖是將變量切分紅了兩個部分,高16位表示讀,低16位表示寫,劃分方式如圖1所示。
圖1. 讀寫鎖狀態的劃分方式
如圖1所示,當前同步狀態表示一個線程已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。讀寫鎖是如何迅速的肯定讀和寫各自的狀態呢?答案是經過位運算。假設當前同步狀態值爲S,寫狀態等於 S & 0x0000FFFF(將高16位所有抹去),讀狀態等於 S >>> 16(無符號補0右移16位)。當寫狀態增長1時,等於S + 1,當讀狀態增長1時,等於S + (1 << 16),也就是S + 0x00010000。
根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S & 0x0000FFFF)等於0時,則讀狀態(S >>> 16)大於0,即讀鎖已被獲取。
寫鎖是一個支持重進入的排它鎖。若是當前線程已經獲取了寫鎖,則增長寫狀態。若是當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程,則當前線程進入等待狀態,獲取寫鎖的代碼以下:
該方法除了重入條件(當前線程爲獲取了寫鎖的線程)以外,增長了一個讀鎖是否存在的判斷。若是存在讀鎖,則寫鎖不能被獲取,緣由在於:讀寫鎖要確保寫鎖的操做對讀鎖可見,若是容許讀鎖在已被獲取的狀況下對寫鎖的獲取,那麼正在運行的其餘讀線程就沒法感知到當前寫線程的操做。所以只有等待其餘讀線程都釋放了讀鎖,寫鎖才能被當前線程所獲取,而寫鎖一旦被獲取,則其餘讀寫線程的後續訪問均被阻塞。
寫鎖的釋放與ReentrantLock的釋放過程基本相似,每次釋放均減小寫狀態,當寫狀態爲0時表示寫鎖已被釋放,從而等待的讀寫線程可以繼續訪問讀寫鎖,同時前次寫線程的修改對後續讀寫線程可見。
讀鎖的獲取與釋放
讀鎖是一個支持重進入的共享鎖,它可以被多個線程同時獲取,在沒有其餘寫線程訪問(或者寫狀態爲0)時,讀鎖總會成功的被獲取,而所作的也只是(線程安全的)增長讀狀態。若是當前線程已經獲取了讀鎖,則增長讀狀態。若是當前線程在獲取讀鎖時,寫鎖已被其餘線程獲取,則進入等待狀態。獲取讀鎖的實現從Java 5到Java 6變得複雜許多,主要緣由是新增了一些功能,好比:getReadHoldCount()方法,返回當前線程獲取讀鎖的次數。讀狀態是全部線程獲取讀鎖次數的總和,而每一個線程各自獲取讀鎖的次數只能選擇保存在ThreadLocal中,由線程自身維護,這使獲取讀鎖的實現變得複雜。所以,這裏將獲取讀鎖的代碼作了刪減,保留必要的部分,代碼以下。
在tryAcquireShared(int unused)方法中,若是其餘線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。若是當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增長讀狀態,成功獲取讀鎖。
讀鎖的每次釋放均(線程安全的,可能有多個讀線程同時釋放讀鎖)減小讀狀態,減小的值是(1 << 16)。
具體的緣由和實現書上有,我不具體研究了。
上一篇講過這個問題。不過這本書給了另一種解決方案:
基於類初始化的解決方案
JVM在類(Class,而不是對象)的初始化階段(即在Class被加載後,且被線程使用以前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖能夠同步多個線程對同一個類的初始化。
基於這個特性,能夠實現另外一種線程安全的延遲初始化方案(這個方案被稱之爲Initialization On Demand Holder idiom):
個人理解:其實這種方式是在初始化階段加鎖一次實現的,而使用volatile的雙重檢查鎖定通常狀況下,也只會加鎖一次(由於第二次進入,if會成功,就不進入synchronize塊了)。在性能上,他們兩個差很少吧。