【JAVA今法修真】 第六章 天道無情,鎖定乾坤

南橘ryc發表於2021-12-24

您好,我是南橘,萬法仙門的掌門,剛剛從九州世界穿越到地球,因為時空亂流的影響導致我的法力全失,現在不得不通過這個平臺向廣大修真天才們借去力量。你們的每一個點贊,每一個關注都是讓我回到九州世界的助力,兄弟萌來為我注入修為吧!關注WX號:南橘ryc

今天是平安夜,祝大家都有一個愉快的夜晚。

“羅妍師姐!研究院中研究元宇宙的元嬰真人羅銘志剛剛渡劫失敗,差點隕落了。”作為兩世宅男,李小庚基本上不會出雲霄殿,但是總能及時的獲取門內各種八卦訊息。

“哦,我知道,他是我哥。”二師姐羅妍永遠是一副冷冰冰的面孔,但是整個萬法仙門都知道她其實經常半夜在後山唱歌。

“額。”李小庚感到有一點尷尬,連忙把手中剛剛從冰庫取出來的西瓜分了一半給羅妍:“那您大哥他沒事吧。”

“死不了。”羅妍接過半囊西瓜,捏了一個法決變出一根勺子開始挖西瓜吃:“最後一道劫雷下來之前,他已經用連人帶天劫給鎖住了。後來掌門出手解決了這件事,不過嘛,在床上躺上三五個月是很正常的。”

“什麼鎖這麼神奇?還能鎖住天劫?”

“小庚同學,我們萬法仙門《Java真經》中的鎖,可是包羅永珍的哦。”雲小霄不知什麼時候突然出現,一把奪過李小庚手裡剩下的半個瓜,囂張的吃了一大口:“小羅妍,給小庚講講我們吧。”

“好的師父。”

一、樂觀鎖 VS 悲觀鎖

在Java中,我們能接觸到各種各樣的鎖,而每種鎖因其特性的不同,在不同的的場景下有著不同的效果。

悲觀鎖樂觀鎖大概是我們聽到最多的兩種鎖了,這兩種鎖的區分更多的是思想上。

對於一個操作,悲觀鎖認為自己在操作過程中,一定有別的執行緒也要來修改這個資料,所以一定會加鎖。而樂觀鎖則不認為會有別的執行緒來干擾自己,所以不需要加鎖。

在Java中,synchronized關鍵字和Lock的實現類都是悲觀鎖,而樂觀鎖一般採用無鎖程式設計,也就是CAS演算法來實現的。

1、1、悲觀鎖


悲觀鎖的實現:

  • 1、執行緒嘗試去獲取鎖
  • 2、執行緒加鎖成功並執行操作,其他執行緒等待,執行緒加鎖失敗則等待獲取鎖(這裡有好幾種辦法,在synchronized中,會有在四種狀態中改變,在下文中我會介紹這四種情況)
  • 3、執行緒執行完畢釋放鎖,其他執行緒獲取鎖

通過圖片和文字,我們能看出悲觀鎖適合寫操作多的場景,加鎖可以確保資料的安全,但是會影響一些操作效率。

1、2、樂觀鎖


這兩張圖是從這位大佬的文章中引用的:不可不說的Java“鎖”事 - 美團技術團隊

樂觀鎖的實現:

  • 1、執行緒直接獲取同步資源資料
  • 2、判斷記憶體中的同步資料是否被其他執行緒修改
  • 3、沒有被修改則直接更新
  • 4、如果被其他執行緒修則選擇報錯或者重試(自旋)

和悲觀鎖不同,樂觀鎖明顯不適合經常進行修改,因為誰也不能保證不會出現資料安全的問題,所以樂觀鎖適合讀操作的場景。對於讀操作來說,加鎖只會影響效率。

上文說到了,樂觀鎖一般採用CAS演算法來實現,那麼我們就來講講什麼是CAS演算法

1、3、CAS演算法

CAS的英語是【Compare and Swap】,比較和交換,單單從這一個片語來看,我們就已經能Get到CAS演算法的核心了。

CAS的演算法涉及三個運算元: 記憶體位置(V)預期原值(A)新值(B)

如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。

換一種說法,當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值(“比較+更新”整體是一個原子操作),否則不會執行任何操作。

在JDK1.5 中新增java.util.concurrent(J.U.C)就是通過實現CAS來實現樂觀鎖的。
我們可以看一下它的重點:


在沒有鎖的機制下需要欄位value要藉助volatile原語,保證執行緒間的資料是可見的。這樣在獲取變數的值的時候才能直接讀取,這就是記憶體的可見性。



從上面這三個圖可以看出,CAS每次從記憶體中讀取資料然後將此資料修改+1後的結果進行CAS操作比較,如果成功就返回結果,否則重試直到成功為止,compareAndSet利用JNI來完成CPU指令的操作。

是不是很複雜?其實一點也不復雜,我們可以這樣理解:CPU去更新一個值,但如果想改的值和原來的值,操作就失敗(因為有其它操作先改變了這個值),然後可以去再次嘗試。如果想改的值和原來一樣,那麼就修改之。

但是,CAS也有一些問題

  • ABA問題

一個執行緒X1從記憶體位置V中取出A,這時候另一個執行緒Y1也從記憶體中取出A,並且Y1進行了一些操作變成了B,然後Y1又將V位置的資料變成A,這時候執行緒X1進行CAS操作發現記憶體中仍然是A,然後X1操作成功。儘管執行緒X1的CAS操作成功,但是不代表這個過程就是沒有問題的。

解決辦法
JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中,利用JNI來檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。

  • 迴圈時間長開銷大
    CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。

  • 只能保證一個共享變數的原子操作
    Java從1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,可以把多個變數放在一個物件裡來進行CAS操作。

Java中的執行緒安全問題至關重要,要想保證執行緒安全,就需要用到樂觀鎖與悲觀鎖。悲觀鎖是獨佔鎖,阻塞鎖。樂觀鎖是非獨佔鎖,非阻塞鎖。什麼情況選擇什麼樣的鎖,就是我們開發人員需要思考的問題了。

二、自旋鎖VS非自旋鎖

我們之前提到了CAS操作如果長時間不成功,會導致其一直自旋,非常浪費效能。但是實際是,自旋是非常有用的。

自旋鎖(spinlock):是指當一個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那麼該執行緒將迴圈等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出迴圈

自旋鎖不會放棄CUP時間片,而是通過自旋等待鎖釋放。

為什麼要自旋,?獲取鎖的執行緒一直處於活躍狀態,但是並沒有執行任何有效的任務,使用這種鎖不是會造成busy-waiting嗎?

因為在我們的程式中,如果存在著大量的互斥同步程式碼,當出現高併發的時候,系統核心態就需要不斷的去掛起執行緒恢復執行緒,頻繁的上下文切換會對我們系統的併發效能有一定影響。在程式的執行過程中鎖定“共享資源“的時間片是極短的,如果僅僅是為了這點時間而去不斷掛起、恢復執行緒的話,消耗的時間可能會更長,那就“撿了芝麻丟了西瓜”了。

自旋等待雖然避免了執行緒切換的開銷,但它要佔用處理器時間。如果鎖被佔用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被佔用的時間很長,那麼自旋的執行緒只會白浪費處理器資源

於是乎,自適應的自旋鎖出現了。

自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK 6中變為預設開啟,並且引入了自適應的自旋鎖(適應性自旋鎖)。

自適應的自旋鎖

自適應自旋鎖的出現使得自旋操作變得聰明起來,不再跟之前一樣死板。所謂的“自適應”意味著對於同一個鎖物件,執行緒的自旋時間是根據上一個持有該鎖的執行緒的自旋時間以及狀態來確定的。例如對於A鎖物件來說,如果一個執行緒剛剛通過自旋獲得到了鎖,並且該執行緒也在執行中,那麼JVM會認為此次自旋操作也是有很大的機會可以拿到鎖,因此它會讓自旋的時間相對延長。但是如果對於B鎖物件自旋操作很少成功的話,JVM甚至可能直接忽略自旋操作。

因此,自適應自旋鎖在一定程度上能強化自旋鎖的效能。

可是,出現了多個執行緒同時爭搶鎖資源,我們也不能總是自旋啊!
於是,java團隊又進行了進化。

三、無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖

學習這四個鎖之前,我們先來了解一下java物件頭Monitor的概念。

synchronized是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java物件頭裡的,Hotspot的物件頭主要包括兩部分資料:Mark Word(標記欄位)、Klass Pointer(型別指標)。

  • Mark Word:預設儲存物件的HashCode,分代年齡和鎖標誌位資訊
  • Klass Point:物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項

每一個Java物件就有一把看不見的鎖,稱為內部鎖或者Monitor鎖。

Monitor是執行緒私有的資料結構,每一個執行緒都有一個可用monitor record列表,同時還有一個全域性的可用列表,每一個被鎖住的物件都會和一個monitor關聯,同時monitor中有一個Owner欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用

synchronized通過Monitor來實現執行緒同步,Monitor是依賴於底層的作業系統的Mutex Lock(互斥鎖)來實現的執行緒同步

為了瞭解這幾個概念,我們可以通過兩個程式碼來看一個:



第一塊程式碼很簡單,看一看位元組碼,非常清楚,一眼就能看出它做了什麼。



再看第二個程式碼,看看java程式碼,非常簡單,和HelloWorld相比只是多了一個synchronize的程式碼塊,但是位元組碼卻大不一樣,可以看出在加鎖的程式碼塊, 多了個 monitorenter , monitorexit

每個物件有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:

  • 1、如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor的所有者。
  • 2、如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.
  • 3、如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權

執行monitorexit的執行緒必須是objectref所對應的monitor的所有者

  • 1、指令執行時,monitor的進入數減1
  • 2、如果減1後進入數為0,那執行緒退出monitor,不再是這個monitor的所有者
  • 3、其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor 的所有權

通過這兩個圖,大家大概就能理解之前的那兩個概念了。

我們知道,高併發的情況,不斷地爭搶鎖,系統核心態就需要不斷的去掛起執行緒恢復執行緒,頻繁的上下文切換會對我們系統的併發效能有一定影響。如果同步程式碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比使用者程式碼執行的時間還要長”。這種方式就是synchronized最初實現同步的方式,這就是JDK 6之前synchronized效率低的原因。這種依賴於作業系統Mutex Lock所實現的鎖我們稱之為“重量級鎖”,JDK6中為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”。

所以目前鎖一共有4種狀態,級別從低到高依次是:無鎖、偏向鎖、輕量級鎖重量級鎖。鎖狀態只能升級不能降級。

這是四種鎖狀態對應的的:Mark Word(標記欄位)內容:

鎖狀態 儲存內容 Mark Word
無鎖 物件的hashCode、物件分代年齡、是否是偏向鎖(0) 01
偏向鎖 偏向執行緒ID、偏向時間戳、物件分代年齡、是否是偏向鎖(1) 01
輕量級鎖 指向棧中鎖記錄的指標 00
重量級鎖 指向互斥量(重量級鎖)的指標 10

3、1無鎖

無鎖的特點就是修改操作在迴圈內進行,執行緒會不斷的嘗試修改共享資源。

如果有多個執行緒修改同一個值,必定會有一個執行緒能修改成功,而其他修改失敗的執行緒會不斷重試直到修改成功,CAS原理及應用即是無鎖的實現。

3、2偏向鎖

偏向鎖是指一段同步程式碼一直被一個執行緒所訪問,那麼該執行緒會自動獲取鎖,降低獲取鎖的代價。

偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖物件是否處於被鎖定狀態。撤銷偏向鎖後恢復到無鎖(標誌位為“01”)或輕量級鎖(標誌位為“00”)的狀態。

3、3輕量級鎖

當鎖是偏向鎖的時候,被另外的執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高效能。

在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝,然後拷貝物件頭中的Mark Word複製到鎖記錄中。

拷貝成功後,虛擬機器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,並將Lock Record裡的owner指標指向物件的Mark Word。

如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位設定為“00”,表示此物件處於輕量級鎖定狀態。

如果輕量級鎖的更新操作失敗了,虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,否則說明多個執行緒競爭鎖。

若當前只有一個等待執行緒,則該執行緒通過自旋進行等待。但是當自旋超過一定的次數,或者一個執行緒在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖。

3、4重量級鎖

升級為重量級鎖時,鎖標誌的狀態值變為“10”,此時Mark Word中儲存的是指向重量級鎖的指標,此時等待鎖的執行緒都會進入阻塞狀態。

四、公平鎖 VS 非公平鎖

4、1公平鎖

公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖,執行緒直接進入佇列中排隊,佇列中的第一個執行緒才能獲得鎖。

公平鎖的優點是:等待鎖的執行緒不會餓死,人人有飯吃,人人有書讀

公平鎖的缺點是:整體吞吐效率相對非公平鎖要低,等待佇列中除第一個執行緒以外的所有執行緒都會阻塞,CPU喚醒阻塞執行緒的開銷比非公平鎖大

4、2、非公平鎖

非公平鎖是多個執行緒加鎖時直接嘗試獲取鎖,獲取不到才會到等待佇列的隊尾等待。如果此時此刻鎖剛好可用,那麼這個執行緒就可以插隊,無阻塞地獲取鎖。

非公平鎖的優點是:可以減少喚起執行緒的開銷,整體的吞吐效率高,因為執行緒有機率不阻塞直接獲得鎖,CPU不必喚醒所有執行緒

非公平鎖的缺點是:處於等待佇列中的執行緒可能會餓死,或者等很久才會獲得鎖

我們可以通過一些原始碼來看一看公平鎖和非公平鎖在java中的應用。

公平鎖FairSync非公平鎖NonfairSync的程式碼


從結構中來看,ReentrantLock裡面有一個內部類Sync,Sync繼承自AQS(AbstractQueuedSynchronizer),新增鎖和釋放鎖的大部分操作實際上都是在Sync中實現的。它有公平鎖FairSync和非公平鎖NonfairSync兩個子類。ReentrantLock預設使用非公平鎖,也可以通過構造器來顯示的指定使用公平鎖。

公平鎖FairSync 非公平鎖NonfairSync

我們用軟體比較一下:

是不是很清晰了?公平鎖和非公平鎖只有一個地方不一樣

閱讀一下注釋:是否返回true取決於頭是否在尾部之前初始化以及頭是否準確(如果當前執行緒在佇列中)

意思就是這個方法主要是判斷當前執行緒是否位於同步佇列中的第一個。如果是則返回true,否則返回false

由此可得,公平鎖通過同步佇列來實現順序獲取鎖,而非公平鎖加鎖時不考慮先後順序,直接嘗試去獲取鎖,所以存在後申請卻先獲得鎖的情況。

五、可重入鎖 VS 非可重入鎖

可重入鎖這個概念也比較好理解,在同一個執行緒在外層方法獲取鎖的時候,再進入該執行緒的內層方法能自動獲取鎖(前提鎖物件得是同一個物件或者class)就是可重入鎖,不能自動獲取那麼這個鎖就是不可重入鎖。

在JAVA中,我們最熟悉的ReentrantLock和synchronized都是可重入鎖。

為什麼可重入鎖可以自動獲得鎖呢?

可重入鎖ReentrantLock:

不可重入鎖NonReentrantLock:

這兩個圖是不是很明顯?

由圖可知,當執行緒嘗試獲取鎖時,可重入鎖先嚐試獲取並更新status值,如果status == 0表示沒有其他執行緒在執行同步程式碼,則把status置為1,當前執行緒開始執行。如果status != 0,則判斷當前執行緒是否是獲取到這個鎖的執行緒,如果是的話執行status+1,且當前執行緒可以再次獲取鎖。而非可重入鎖是直接去獲取並嘗試更新當前status的值,如果status != 0的話會導致其獲取鎖失敗,當前執行緒阻塞。

釋放鎖時,可重入鎖同樣先獲取當前status的值,在當前執行緒是持有鎖的執行緒的前提下。如果status-1 == 0,則表示當前執行緒所有重複獲取鎖的操作都已經執行完畢,然後該執行緒才會真正釋放鎖。而非可重入鎖則是在確定當前執行緒是持有鎖的執行緒之後,直接將status置為0,將鎖釋放。

六、獨享鎖 VS 共享鎖

獨享鎖和共享鎖這個概念,可以類比為讀寫鎖。

舉個例子,A執行緒獲得資料ZZZ的鎖,如果加鎖後其他的執行緒不能再對ZZZ加任何形式的鎖,也不能對它進行讀寫,那麼說明ZZZ上的是排他鎖。

如果執行緒A獲得資料ZZZ上的鎖以後,則其他執行緒還能對ZZZ再加共享鎖,獲得共享鎖的執行緒還能讀資料,只是不能修改資料,那麼說明ZZZ上的是共享鎖。

我們可以看看讀寫鎖ReentrantReadWriteLock

讀寫鎖裡面有兩把鎖,一把是ReadLock,一把是WriteLock,現在我們不知道里面是什麼樣子的

ReadLock:


WriteLock:

我們驚訝的發現了一個老熟人state,我們總是能看到他。

在獨享鎖中state這個值通常是0或者1(如果是重入鎖的話state值就是重入的次數),在共享鎖中state就是持有鎖的數量。但是在ReentrantReadWriteLock中有讀、寫兩把鎖,所以需要在一個整型變數state上分別描述讀鎖和寫鎖的數量(或者也可以叫狀態)。於是將state變數“按位切割”切分成了兩個部分,高16位表示讀鎖狀態(讀鎖個數),低16位表示寫鎖狀態(寫鎖個數)。


從寫鎖的這一段我們可以看出,它首先判斷是否已經有執行緒持有了鎖。如果已經有執行緒持有了鎖(c!=0),則檢視當前寫鎖執行緒的數目,如果寫執行緒數為0(即此時存在讀鎖)或者持有鎖的執行緒不是當前執行緒就返回失敗。


從讀鎖中又能發現,如果其他執行緒已經獲取了寫鎖,則當前執行緒獲取讀鎖失敗,進入等待狀態。如果當前執行緒獲取了寫鎖或者寫鎖未被獲取,則當前執行緒(執行緒安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖。

“鎖的功能和用法竟然有這麼多的嗎?”李小庚長吸了一口冷氣,本來只是簡單地吃一個瓜,沒想到被知識大禮包砸中了。

“其實我們的《Java真經》本身已經對鎖本身進行了良好的封裝,降低了鬥法中使用難度,這也是我那個倒黴哥哥能活下來的原因。”羅妍舀完了最後一塊瓜肉,滿足的伸了個懶腰:“好了師弟,即使封裝的再好,熟悉鎖的底層原理,才能在不同場景下選擇最適合的鎖。平常在修行的過程中,不要只追求結果的實現,多研究研功法究原始碼才能讓你對它的理解更加深刻。”說罷,一個轉身便向實驗室走去。

“你二師姐可是我們萬法仙門出了名的愛專研,經常能夠發現功法中的漏洞。”雲小霄毫無風度的蹲在一邊,感嘆道:“所以人家才能在高手雲集的結丹組大比中獲得的冠軍啊!”

“嘿嘿,下屆的冠軍就是我了。”

“哦,是嗎?被一招秒殺的築基組亞軍李小庚同學。”

“喂喂喂,別揭短行不行。”

相關文章