前言碎語
Synchronized和 ReentrantLock 大家應該都不陌生了,作為java中最常用的本地鎖,最初版本中 ReentrantLock 的性能是遠遠強於 Synchronized 的,後續java在一次次的版本迭代中對 Synchronized 進行了大量的優化,直到 jdk1.6 之後,兩種鎖的性能已經相差無幾,甚至 Synchronized 的自動釋放鎖會更好用。
在面試時被問到 Synchronized 和 ReentrantLock 的使用選擇時,很多朋友都脫口而出的說用 Synchronized ,甚至在我面試的時候問面試者,也很少有人能夠答出所以然來,moon 想說,這可不一定,只對標題感興趣的同學可以直接劃到最後,我可不是標題黨~
Synchronized使用
在 java 代碼中 synchronized 的使用是非常簡單的
1.直接貼在方法上2.貼在代碼塊兒上
程序運行期間,Synchronized那一塊兒代碼發生麼什麼?
來看一張圖
在多線程運行過程中,線程會去先搶對象的監視器,這個監視器是對象獨有的,其實就相當於一把鑰匙,搶到了,那你就獲得了當前代碼塊兒的執行權。
其他沒有搶到的線程會進入隊列(SynchronizedQueue)當中等待,等待當前線程執行完後,釋放鎖.
最後當前線程執行完畢後通知出隊然後繼續重複當前過程.
從 jvm 的角度來看 monitorenter 和 monitorexit 指令代表著代碼的執行與結束。
SynchronizedQueue:
SynchronizedQueue 是一個比較特殊的隊列,它沒有存儲功能,它的功能就是維護一組線程,其中每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作。因此此隊列內部其 實沒有任何一個元素,或者說容量是0,嚴格說並不是一種容器。由於隊列沒有容量,因此不能調用 peek 操作,因為只有移除元素時才有元素。
舉個例子:
喝酒的時候,先把酒倒入酒盅,然後再倒入酒杯,這就是正常的隊列。
喝酒的時候,把酒直接倒入酒杯,這就是 SynchronizedQueue。
這個例子應該很清晰易懂了,它的好處就是可以直接傳遞,省去了一個第三方傳遞的過程。
聊聊細節,鎖升級的過程
在 jdk1.6 以前,Synchronized 是一個重量級鎖,還是先貼一張圖
這就是為什麼說,Synchronized 是一個重量級鎖的原因,因為每一次鎖的資源都是直接和 cpu 去申請的,而 cpu 的鎖數量是固定的,當 cpu 鎖資源使用完後還會進行鎖等待,這是一個非常耗時的操作。
但是在jdk1.6,針對代碼層面進行了大量的優化,也就是我們常說的鎖升級的過程。
這就是一個鎖升級的過程,我們簡單的說說:
無鎖:對象一開始就是無鎖狀態。偏向鎖:相當於給對象貼了一個標籤(將自己的線程 id 存入對象頭中),下次我再進來時,發現標籤是我的,我就可以繼續使用了。自旋鎖:想像一下有一個廁所,裡面有一個人在,你很想上但是只有一個坑位,所以你只能徘徊等待,等那個人出來以後,你就可以使用了。這個自旋是使用 cas 來保證原子性的,關於 cas 我這裡就不再贅述了。重量級鎖:直接向 cpu 去申請申請鎖,其他的線程都進入隊列中等待。鎖升級是什麼時候發生的?
偏向鎖:一個線程獲取鎖時會由無鎖升級為偏向鎖自旋鎖:當產生線程競爭時由偏向鎖升級為自旋鎖,想像一下 while(true) ;重量級鎖:當線程競爭到達一定數量或超過一定時間時,晉升為重量級鎖鎖的信息是記錄在哪裡的?
這張圖是對象頭中 markword 的數據結構,鎖的信息就是在這裡存放的,很清楚的表明了鎖在升級的時候鎖信息的變動,其實就是通過二進位的數值,來對對象進行一個標記,每個數值代表一種狀態。
既然synchronized有鎖升級那麼有鎖降級嗎?
這個問題和我們的題目就有很大的關聯了。
在 HotSpot 虛擬機中是有鎖降級的,但是僅僅只發生在 STW 的時候,只有垃圾回收線程能夠觀測到它,也就是說,在我們正常使用的過程中是不會發生鎖降級的,只有在 GC 的時候才會降級。
所以題目的答案,你懂了嗎?哈哈,我們接著往下走。
ReentrantLock的使用
ReentrantLock 的使用也是非常簡單的,與 Synchronized 的不同就是需要自己去手動釋放鎖,為了保證一定釋放,所以通常都是和 try~finally 配合使用的。
ReentrantLock的原理
ReentrantLock 意為可重入鎖,說起 ReentrantLock 就不得不說 AQS ,因為其底層就是使用 AQS去實現的。
ReentrantLock有兩種模式,一種是公平鎖,一種是非公平鎖。
公平模式下等待線程入隊列後會嚴格按照隊列順序去執行非公平模式下等待線程入隊列後有可能會出現插隊情況
這就是ReentrantLock的結構圖,我們看這張圖其實是很簡單的,因為主要的實現都交給AQS去做了,我們下面著重聊一下AQS。
AQS
AQS(AbstractQueuedSynchronizer): AQS 可以理解為就是一個可以實現鎖的框架。
簡單的流程理解:
公平鎖:
第一步:獲取狀態的 state 的值。如果 state=0 即代表鎖沒有被其它線程佔用,執行第二步。如果 state!=0 則代表鎖正在被其它線程佔用,執行第三步。第二步:判斷隊列中是否有線程在排隊等待。如果不存在則直接將鎖的所有者設置成當前線程,且更新狀態 state 。如果存在就入隊。第三步:判斷鎖的所有者是不是當前線程。如果是則更新狀態 state 的值。如果不是,線程進入隊列排隊等待。非公平鎖:
第一步:獲取狀態的 state 的值。如果 state=0 即代表鎖沒有被其它線程佔用,則設置當前鎖的持有者為當前線程,該操作用 CAS 完成。如果不為0或者設置失敗,代表鎖被佔用進行下一步。此時獲取 state 的值,如果是,則給state+1,獲取鎖如果不是,則進入隊列等待如果是0,代表剛好線程釋放了鎖,此時將鎖的持有者設為自己如果不是0,則查看線程持有者是不是自己讀完以上的部分相信你對AQS已經有了一個比較清楚的概念了,所以我們來聊聊小細節。
AQS使用state同步狀態(0代表無鎖,1代表有),並暴露出 getState 、 setState 以及 compareAndSet 操作來讀取和更新這個狀態,使得僅當同步狀態擁有一個期望值的時候,才會被原子地設置成新值。
當有線程獲取鎖失敗後,AQS是通過一個雙向的同步隊列來完成同步狀態的管理,就被添加到隊列末尾。
這是定義頭尾節點的代碼,我們可以先使用 volatile去修飾的,就是保證讓其他線程可見,AQS 實際上就是修改頭尾兩個節點來完成入隊和出隊操作的。
AQS 在鎖的獲取時,並不一定只有一個線程才能持有這個鎖,所以此時有了獨佔模式和共享模式的區別,我們本篇文章中的 ReentrantLock使用的就是獨佔模式,在多線程的情況下只會有一個線程獲取鎖。
獨佔模式的流程是比較簡單的,就根據state是否為0來判斷是否有線程已經獲得了鎖,沒有就阻塞,有就繼續執行後續代碼邏輯。
共享模式的流程根據state是否大於0來判斷是否有線程已經獲得了鎖,如果不大於0,就阻塞,如果大於0,通過CAS的原子操作來自減state的值,然後繼續執行後續代碼邏輯。
ReentrantLock和Synchronized的區別
其實ReentrantLock和Synchronized最核心的區別就在於Synchronized適合於並發競爭低的情況,因為Synchronized的鎖升級如果最終升級為重量級鎖在使用的過程中是沒有辦法消除的,意味著每次都要和cpu去請求鎖資源,而ReentrantLock主要是提供了阻塞的能力,通過在高並發下線程的掛起,來減少競爭,提高並發能力,所以我們文章標題的答案,也就顯而易見了。synchronized是一個關鍵字,是由jvm層面去實現的,而ReentrantLock是由java api去實現的。synchronized是隱式鎖,可以自動釋放鎖,ReentrantLock是顯式鎖,需要手動釋放鎖。ReentrantLock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷。ReentrantLock可以獲取鎖狀態,而synchronized不能。說說標題的答案
其實題目的答案就在上一欄目的第一條,也是核心的區別,synchronized升級為重量級鎖後無法在正常情況下完成降級,而ReentrantLock是通過阻塞來提高性能的,在設計模式上就體現出了對多線程情況的支持。