java中的鎖機制

java中的鎖主要有兩種
1.synchronize
2.lock

從本質來看,synchronize基於jvm保證數據的同步,lock基於硬件,依賴cpu指令
synchronize鎖的作用範圍
1.作用在普通方法(鎖的是當前對象的實例)
2.作用在靜態方法(鎖的是當前類的實例)
3.作用在代碼塊(鎖的是括號內的方法)
在學習中我們都知道synchronize是一個重量級鎖,但是隨着對synchronize進行優化,顯得其不再那麼重

java對象頭

synchronize鎖信息是存在java對象頭裏邊的
Hotspot虛擬機java對象頭主要有兩個部分Mark word(標記字段)和Klass point(指針類型)
Klass point是對象指向類元數據的指針,對象通過這個指針來判斷這個對象是哪個類的實例
Mark word用於存儲對象運行時的數據,如哈希碼,GC分代年齡,鎖狀態標誌,線程持有的鎖,偏向線程id,偏向時間戳等(java對象頭一般佔有兩個機器碼,數組類型佔三個機器碼)

Monitor

可以是一個同步機制或者是一個對象,每一個對象都有成爲Monitor的潛質,在java設計中,java對象自身就帶一把鎖,叫做內置鎖或者說Monitor鎖
Monitor是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局可用列表,每一個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的lockWord指向monitor的起始地址),同時monitor中有一個owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程佔用

鎖的類型

所的類型一共有如下幾種
1.公平鎖/非公平鎖
2.可重入鎖
3.獨佔鎖/共享鎖
4.互斥鎖/讀寫鎖
5.樂觀鎖/悲觀鎖
6.分段鎖
7.偏向鎖/輕量級鎖/重量級鎖
8.自旋鎖

在synchronize優化的過程中引入了許多鎖

1.自旋鎖的引入
synchronize鎖還有另外一個名稱是對象監視器
當多個線程一起訪問某個對象監視器的時候,對象監視器會將這些請求存儲在不同的容器中。

1、 Contention List:競爭隊列,所有請求鎖的線程首先被放在這個競爭隊列中
2、 Entry List:Contention List中那些有資格成爲候選資源的線程被移動到Entry List中
3、 Wait Set:哪些調用wait方法被阻塞的線程被放置在這裏
4、 OnDeck:任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被成爲OnDeck
5、 Owner:當前已經獲取到所資源的線程被稱爲Owner
6、 !Owner:當前釋放鎖的線程

ContentionList並不是真正意義上的一個隊列。僅僅是一個虛擬隊列,它只有Node以及對應的Next指針構成,並沒有Queue的數據結構。每次新加入Node會在隊頭進行,通過CAS改變第一個節點爲新增節點,同時新增階段的next指向後續節點,而取數據都在隊列尾部進行。

JVM每次從隊列的尾部取出一個數據用於鎖競爭候選者(OnDeck),但是併發情況下,ContentionList會被大量的併發線程進行CAS訪問,爲了降低對尾部元素的競爭,JVM會將一部分線程移動到EntryList中作爲候選競爭線程。Owner線程會在unlock時,將ContentionList中的部分線程遷移到EntryList中,並指定EntryList中的某個線程爲OnDeck線程(一般是最先進去的那個線程)。Owner線程並不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權利交個OnDeck,OnDeck需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在JVM中,也把這種選擇行爲稱之爲“競爭切換”。

OnDeck線程獲取到鎖資源後會變爲Owner線程,而沒有得到鎖資源的仍然停留在EntryList中。如果Owner線程被wait方法阻塞,則轉移到WaitSet隊列中,直到某個時刻通過notify或者notifyAll喚醒,會重新進去EntryList中。

處於ContentionList、EntryList、WaitSet中的線程都處於阻塞狀態,該阻塞是由操作系統來完成的(Linux內核下采用pthread_mutex_lock內核函數實現的)。該線程被阻塞後則進入內核調度狀態,會導致系統在用戶和內核之間進行來回切換,嚴重影響鎖的性能。爲了緩解上述性能問題,JVM引入了自旋鎖。原理非常簡單,如果Owner線程能在很短時間內釋放鎖資源,那麼哪些等待競爭鎖的線程可以稍微等一等(自旋)而不是立即阻塞,當Owner線程釋放鎖後可立即獲取鎖,進而避免用戶線程和內核的切換。但是Owner可能執行的時間會超過設定的閾值,爭用線程在一定時間內還是獲取不到鎖,這是爭用線程會停止自旋進入阻塞狀態。基本思路就是先自旋等待一段時間看能否成功獲取,如果不成功再執行阻塞,儘可能的減少阻塞的可能性,這對於佔用鎖時間比較短的代碼塊來說性能能大幅度的提升!

但是有個頭大的問題,何爲自旋?其實就是執行幾個空方法,稍微等一等,也許是一段時間的循環,也許是幾行空的彙編指令,其目的是爲了佔着CPU的資源不釋放,等到獲取到鎖立即進行處理。但是如何去選擇自旋的執行時間呢?如果自旋執行時間太長,會有大量的線程處於自旋狀態佔用CPU資源,進而會影響整體系統的性能。因此自旋的週期選的額外重要!

JVM對於自旋週期的選擇,基本認爲一個線程上下文切換的時間是最佳的一個時間,同時JVM還針對當前CPU的負荷情況做了較多的優化
1、 如果平均負載小於CPUs則一直自旋
2、 如果有超過(CPUs/2)個線程正在自旋,則後來線程直接阻塞
3、 如果正在自旋的線程發現Owner發生了變化則延遲自旋時間(自旋計數)或進入阻塞
4、 如果CPU處於節電模式則停止自旋
5、 自旋時間的最壞情況是CPU的存儲延遲(CPU A存儲了一個數據,到CPU B得知這個數據直接的時間差)
6、 自旋時會適當放棄線程優先級之間的差異

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章