筆者最近在學習Java多線程的一些基礎知識,淺談一些自己關於Java鎖的一些理解 Java鎖是用來乾什麼的? 我們做一個程式,終歸的目的,就是想讓程式按我們想要的方式來,但是在多線程場景下,很多自己很難預計的到的事情會發生,導致數據的不安全,這時候我們會想到一些方法來解決數據不一致的問題: 避免數據 ...
筆者最近在學習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機制更加的靈活。