筆者最近在學習Java多執行緒的一些基礎知識,淺談一些自己關于Java鎖的一些理解
Java鎖是用來干什么的?
我們做一個程式,終歸的目的,就是想讓程式按我們想要的方式來,但是在多執行緒場景下,很多自己很難預計的到的事情會發生,導致資料的不安全,這時候我們會想到一些方法來解決資料不一致的問題:
- 避免資料不一致(ThreadLocal)
- 排隊
- 投票
而加鎖就是排隊的一種實作方式,
Java鎖都有什么?
Java的鎖大概分為兩種
- Synchronized
- Lock
從JVM角度看Synchronized
今天我們避過Synchronized關鍵字的三種使用方法,避開monitorenter指令和monitorexit指令,避開ACC_SYNCHRONIZED標志這些話題不談,只說說Synchronized關鍵字到底做了什么,
本質上來說,Synchronized底層是基于Lock-Free佇列的,我們從無到有再到優化來剖析一下Synchronized關鍵字,
首先我們必須要明確,在給物件監視器加上Synchronized關鍵字以后,當有多個執行緒同時來請求這個物件監視器的時候,物件監視器會將所有的執行緒分成幾類來處理 大家可以想象一個場景:就是很多人同時去搶一個Offer的時候,會根據你的流程狀態把面試人員分成很多批
- 競爭佇列(筆面試流程中)
首先我們會把所有執行緒放進競爭佇列,這個競爭佇列嚴格意義上并不是Queue,而是一個基于Node和next指標的一個鏈表結構,所有入隊新進執行緒會放在鏈表頭節點的位置,而所有出隊的執行緒則是在鏈表尾節點的位置進行CAS的出隊操作,很明顯這是一個Lock-Free佇列,而能從競爭佇列中拿走執行緒的執行緒只有Owner執行緒,Owner執行緒就是正在拿著鎖的執行緒,它會選擇合適的候選人執行緒,然后把它們放進Entry-List,
- Entry-List(備胎池)
就像某公司的錄用排序中一樣,進了該佇列并不是說一定能拿到資源,讓執行緒進去這個佇列只是為了避免執行緒頻繁的在競爭佇列隊尾沖突,然后進入錄用池以后,會 (非公平) 隨機的拿一個人的簡歷進行錄用(也就是設定成Ready執行緒),這個執行緒如果拿到了Offer就變成Owner執行緒 (上岸) ,如果沒有拿到那就回到錄用池,礙于公平的情面,會把這個執行緒放在Entry-List的隊頭,
- WaitSet(考慮Offer的池子)
如果拿到Offer的人(Owner執行緒)說wait!wait!我要考慮一下,(呼叫wait()方法)那這個執行緒便會被扔進waitSet佇列,當你考慮清楚以后會被重新塞進Entry-List進行流程,
- OnDeck(就是Ready執行緒)(口頭Offer)
正在競爭鎖的執行緒就是Ready執行緒(非公平),
- Owner執行緒(拿到Offer的人)
- !Owner執行緒(毀Offer)
自旋鎖
我們必須要想清楚一點,倘若將和HR交流/聯系不上看作是用戶態/阻塞態,競爭佇列、備胎池、考慮Offer的池子這三個批次的人歸根到底是沒有Offer的,處于這些批次的人是處于阻塞態的(一般聯系不上HR的),為了讓自己的應聘流程沒有那么慢,我們要經常催促HR,也就是輪詢HR到底Offer輪不輪得到我,這個輪詢周期非常值得考究,因為輪詢會占住HR不放,所以當輪詢很多次結果以后,會斷開聯系,也就是進入阻塞態,
翻譯成鎖就是,為了避免執行緒進入阻塞態,得不到鎖的執行緒先自旋,但是自旋一段時間后如果獲得不了鎖,就進入阻塞態
當然面試程序一定是想要公平,卻非公平的
不公平的地方在哪里呢?
經常詢問HR的人可能會引起注意,直接被錄取,這對一直在競爭佇列排隊的人不公平,甚至有可能直接搶走Offer,對處于口頭Offer狀態的人不公平
執行緒進入佇列前先嘗試自旋,如果直接獲得鎖,對等待佇列的執行緒和Ready執行緒不公平
偏向鎖
當然上面的面試場景一般都是大廠場景,很多小廠愿意去面試的人沒幾個,甚至只有你一個(開心嗎?),當你第一次能面試過這家以后,可以再來面試, (可重入) ,再來面試總是能過的(當然我們的假設是別人不要面子),也就是無競爭下,希望你不要再走面試流程了,直接過就可以了,這時候就設計了偏向鎖,
偏向鎖直接去掉了進入流程 加鎖/解鎖 的程序,因為可重入鎖雖然很好,但是加鎖/解鎖程序中設計的CAS操作其實是很影響性能的,
CAS操作為什么影響性能呢?
首先CAS在失敗的時候的自旋操作會占住CPU資源,其次,CAS會造成一些本地延遲,因為在多處理器場景下,每個核會有自己的L1快取,然后通過總線和主存連接起來,如果Core1改變了一些值,Core2拿到這個資料的時候資料會失效,最新的資料同步通信程序會產生快取一致性流量,太大的快取一致性流量會導致總線的壓力太大,成為性能的瓶頸,
從JVM角度來看Lock
今天我們避開Lock的使用方法,Lock從其實作來看主要是通過實作Lock介面,而Lock介面的所有操作都放在了Sync類中,Sync類則是AQS的一個子類,所以其基本思想完全是繼承自AQS的,那么AQS的思想又是什么呢?
淺談AQS
AQS的基本思想是當執行緒請求一個資源的時候,如果資源空閑,就直接給這個執行緒,并鎖住資源,如果資源已經被鎖,則將執行緒加入一個基于阻塞的CLH佇列,CLH佇列是一個虛擬的雙向佇列,鎖釋放的時候會喚醒執行緒,AQS可以實作成獨占式,比如ReentrantLock,也可以實作成共享式,比如信號量、讀寫鎖、倒計時器等,
當我們每次呼叫Lock()方法的時候,默認的會進行非公平鎖獲取的方法,先會判斷當前鎖的狀態,如果當前鎖狀態c==0,也就是空著的話,就直接獲得該鎖,然后該鎖的acquire屬性會+1,unlock()的時候會-1,當為0的時候為空,如果當前鎖狀態不是0,要判斷是否是自己占有鎖,如果是自己的話,就給值++,避免CAS操作,也就是實作了偏向鎖,
得不到鎖的執行緒會被包裝成Node呼叫addWriter()進等待佇列,如果有隊尾的話,會CAS將當前執行緒更新為隊尾,如果沒有隊尾的話,就回圈CAS知道加入隊尾,addWriter()回傳的執行緒進行阻塞,阻塞前先嘗試tryAcquire()能否獲得鎖,每個節點根據詢問前繼節點是否阻塞來決定是否阻塞,
說了這么多,我們就來比較一下Synchronized和Lock鎖吧
- Synchronized
這是一個基于Lock-Free等待佇列的關鍵字,JVM分的更加仔細,將等待佇列分成了好幾個部分,為了加快出列的速度,并且Synchronized實作了自旋鎖,但這是基于JVM的指令實作的,
- Lock
這是一個基于阻塞的CLH等待佇列,佇列內的所有操作都是基于CAS的,并且對已經獲得了鎖的執行緒可以實作偏向鎖,但是并沒有實作自旋鎖,只能僵硬的等待,好的一點是Lock更適應于擴展,可以擴展成讀寫鎖、公平鎖、非公平鎖等,另外區別于wait/notify()機制的是Condition機制更加的靈活,
Synchronized 和 Lock 鎖在JVM中的實作原理以及代碼決議
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/161020.html
標籤:Java
