動態高並發時為什麼推薦ReentrantLock而不是Synchronized?

2020-12-13 moon聊技術

前言碎語

SynchronizedReentrantLock 大家應該都不陌生了,作為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是通過阻塞來提高性能的,在設計模式上就體現出了對多線程情況的支持。

相關焦點

  • JAVA並發編程:concurrent提供了哪些比synchronized更高效的鎖
    通用方法講解這三種鎖都實現了lock接口,先解讀一下Lock接口的方法及含義:void lock(); 方法,阻塞方法,直到可以獲取到鎖;void lockInterruptibly()方法,阻塞方法,直到可以獲取到鎖或者被其他線程中斷;boolean tryLock();嘗試獲取鎖,獲取不到立刻返回boolean
  • 【第17期】面試官:Java中提供了synchronized,為什麼還要提供Lock呢?高並發
    synchronized的局限性 如果我們的程序使用synchronized關鍵字發生了死鎖時,synchronized關鍵是是無法破壞「不可剝奪」這個死鎖的條件的。這是因為synchronized申請資源的時候, 如果申請不到, 線程直接進入阻塞狀態了, 而線程進入阻塞狀態, 啥都幹不了, 也釋放不了線程已經佔有的資源。
  • 徹底理解ReentrantLock可重入鎖的使用
    java除了使用關鍵字synchronized外,還可以使用ReentrantLock實現獨佔鎖的功能。而且ReentrantLock相比synchronized而言功能更加豐富,使用起來更為靈活,也更適合複雜的並發場景。這篇文章主要是從使用的角度來分析一下ReentrantLock。
  • 這篇 ReentrantLock 你可以看得懂!
    回答一個問題在開始本篇文章的內容講述前,先來回答我一個問題,為什麼 JDK 提供一個 synchronized 關鍵字之後還要提供一個 Lock 鎖,這不是多此一舉嗎?難道 JDK 設計人員都是沙雕嗎?
  • 「從入門到放棄-Java」並發編程-鎖-synchronized
    簡介上篇【從入門到放棄-Java】並發編程-線程安全中,我們了解到,可以通過加鎖機制來保護共享對象,來實現線程安全。synchronized是java提供的一種內置的鎖機制。通過synchronized關鍵字同步代碼塊。
  • Lock鎖 精講
    Lock為什麼synchronized不夠用,還需要Lock       Lock和synchronized這兩個最常見的鎖都可以達到線程安全的目的,但是功能上有很大不同。       Lock並不是用來代替synchronized的而是當使用synchronized不滿足情況或者不合適的時候來提供高級功能的為什麼synchronized不夠用2.Lock鎖的意義與使用synchronized方法和語句相比, Lock實現提供了更廣泛的鎖操作。
  • Java面試熱點:深入學習並發編程中的synchronized(後三章)
    同時JVM中也維護著global locklist。當線程需要ObjectMonitor對象時,首先從線程自身的free表中申請,若存在則使用,若不存在則從global list中申請。CAS獲取共享變量時,為了保證該變量的可見性,需要使用volatile修飾。結合CAS和volatile可以實現無鎖並發,適用於競爭不激烈、多核 CPU 的場景下。 1. 因為沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一。 2.
  • Java中synchronized鎖和lock鎖的比較
    Java並發之顯式鎖和隱式鎖的區別在面試的過程中有可能會問到:在Java並發編程中,鎖有兩種實現:使用隱式鎖和使用顯示鎖分別是什麼?兩者的區別是什麼?所謂的顯式鎖和隱式鎖的區別也就是說說Synchronized(下文簡稱:sync)和lock(下文就用ReentrantLock來代之lock)的區別。
  • Java面試熱點學習:深入並發編程中的synchronized(前三章)
    小結並發編程時,會出現可見性問題,當一個線程對共享變量進行了修改,另外的線程並沒有立即看到修改後的最新值。比如一個線程在執行13: iadd時,另一個線程又執行9: getstatic。會導致兩次number++,實際上只加了1。小結並發編程時,會出現原子性問題,當一個線程對共享變量操作到一半時,另外的線程也有可能來操作共 享變量,幹擾了前一個線程的操作。
  • Java中可重入鎖ReentrantLock原理剖析
    1.1、Lock接口Lock接口,是對控制並發的工具的抽象。它比使用synchronized關鍵詞更靈活,並且能夠支持條件變量。它是一種控制並發的工具,一般來說,它控制對某種共享資源的獨佔。也就是說,同一時間內只有一個線程可以獲取這個鎖並佔用資源。其他線程想要獲取鎖,必須等待這個線程釋放鎖。在Java實現中的ReentrantLock就是這樣的鎖。
  • 深入理解ReentrantLock的實現原理
    來源:https://url.cn/5D162qMReentrantLock簡介ReentrantLock是Java在JDK1.5引入的顯式鎖,在實現原理和功能上都和內置鎖(synchronized)上都有區別,在文章最後我們再比較這兩個鎖。
  • Java並發編程:synchronized
    假如兩個線程分別用thread-1和thread-2表示,某一時刻,thread-1和thread-2都讀取到了數據X,那麼可能會發生這種情況:thread-1去檢查資料庫中是否存在數據X,然後thread-2也接著去檢查資料庫中是否存在數據X。結果兩個線程檢查的結果都是資料庫中不存在數據X,那麼兩個線程都分別將數據X插入資料庫表當中。
  • JAVA 並發編程Synchronized鎖的是什麼?
    當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執行完畢或者代碼塊執行完畢,這個線程才會釋放該對象的鎖,其他線程才能執行這個方法或者代碼塊。下面我們將創建兩個線程A,B來同時訪問一個對象:A從帳戶裡取錢,B從帳戶裡存錢。
  • JAVA中synchronized與static synchronized 的區別
    實際上,在類中某方法或某代碼塊中有 synchronized,那麼在生成一個該類實例後,改類也就有一個監視快,放置線程並發訪問改實例synchronized保護快,而static synchronized則是所有該類的實例公用一個監視快了,也也就是兩個的區別了,也就是synchronized相當於 this.synchronized,而static synchronized相當於Something.synchronized
  • Java並發編程:synchronized關鍵字的實現原理——鎖膨脹過程
    t2加鎖前的狀態和t1解鎖後是一樣的,偏向鎖解鎖不會改變對象頭,接著對其加鎖,判斷當前線程id和對象頭中的線程id是否相同,由於不相同所以會做偏向撤銷(即將狀態修改為001無鎖狀態)並膨脹為輕量鎖(實際上對象第一次加鎖時,也有這個判斷,接著會判斷是不是匿名偏向,即是不是可偏向模式且第一次加鎖,是則直接獲取偏向鎖),狀態改為
  • 面試官:不使用synchronized和lock,如何實現一個線程安全的單例?
    稍微了解一點單例的朋友也都知道實現單例是要考慮並發問題的,一般情況下,我們都會使用synchronized來保證線程安全。那麼,如果有這樣一道面試題:不使用synchronized和lock,如何實現一個線程安全的單例?你該如何回答?C類應聘者:可以使用餓漢模式實現單例。
  • ReentrantLock核心原理,絕對乾貨
    簡單應用ReentrantLock 的使用相比較 synchronized 會稍微繁瑣一點,所謂顯示鎖,也就是你在代碼中需要主動的去進行 lock 操作。lock.lock () 就是在顯式的上鎖。第二個是有參數的 tryLock 方法,通過傳入時間和單位,來控制等待獲取鎖的時長。如果超過時間未能獲取鎖則放回 false,反之返回 true。
  • 「原創」Java並發編程系列09|基礎乾貨
    基本上所有的併發模式在解決線程安全問題時,都採用「序列化訪問臨界資源」的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。通常來說,是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他線程繼續訪問。在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。
  • Java synchronized 詳解
    參與構建火車票業務系統的底層技術支持體系,個人對並發編程、分布式系統等技術點感興趣。前言記得剛剛學習Java的時候,遇到多線程情況首先想到的就是synchronized。相對於當時的我們來說,synchronized是那麼的神奇而強大,同時它也成為我們解決多線程情況百試不爽的良藥。
  • Java 並發編程:Synchronized 及其實現原理
    ),所以即使test和test2屬於不同的對象,但是它們都屬於SynchronizedTest類的實例,所以也只能順序的執行method1和method2,不能並發執行。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:如果monitor的進入數為0,則該線程進入monitor,然後將進入數設置為1,該線程即為monitor的所有者。如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.