詳解Java中的悲觀鎖與樂觀鎖
一、悲觀鎖
悲觀鎖顧名思義是從悲觀的角度去思考問題,解決問題。它總是會假設當前情況是最壞的情況,在每次去拿數據的時候,都會認為數據會被別人改變,因此在每次進行拿數據操作的時候都會加鎖,如此一來,如果此時有別人也來拿這個數據的時候就會阻塞知道它拿到鎖。在Java中,Synchronized和ReentrantLock等獨占鎖的實現機制就是基于悲觀鎖思想。在數據庫中也經常用到這種鎖機制,如行鎖,表鎖,讀寫鎖等,都是在操作之前先上鎖,保證共享資源只能給一個操作(一個線程)使用。
由于悲觀鎖的頻繁加鎖,因此導致了一些問題的出現:比如在多線程競爭下,頻繁加鎖、釋放鎖導致頻繁的上下文切換和調度延時,一個線程持有鎖會導致其他線程進入阻塞狀態(tài),從而引起性能問題。
二、樂觀鎖
樂觀鎖從字面上看是從積極,樂觀的角度去看待問題,因此它認為數據一般不會產生沖突,因此一般不加鎖,當數據進行提交更新時,才會真正對數據是否產生沖突進行監(jiān)測。如果發(fā)生沖突,就返回給用戶錯誤信息,由用戶來決定如何去做,主要有兩個步驟:沖突檢測和數據更新。
三、CAS
CAS(compare and set),比較和更新。CAS是樂觀鎖的技術實現,當多個線程嘗試使用CAS同時來更新同一個變量,只有一個線程能夠更新變量值,而其他的線程都會失敗,失敗的線程并不會被掛起,告知這次競爭失敗,可以再次嘗試。
CAS操作包含三個操作數:
- 需要讀寫的內存位置(V)
- 需要比較的預期原值(A)
- 擬寫入的新值(B)

如果內存位置V的值與原預期值A相匹配,那么處理器就會自動將該位置更新為新值B,否則處理器不做任何處理。樂觀鎖是一種思想,CAS是這種思想的一種實現方法。Java中對CAS支持,在jdk1.5之后新增java.util.concurrent(J.U.C)就是建立CAS基礎上,CAS是一種非阻塞的實現,例如:Atomic
四、AtomicXXX
在Java中,提供了一些原子化的操作類型,如下操作
private volatile int value;
public final int get() {
return value;
}
讀取的值,value是聲明為volatile的,就可以保證在沒有鎖的情況下,線程可見性
在涉及到數據變更,以incrementAndGet實例:++i操作
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
采用的CAS的操作,每次讀取內存中的數據,讓后將數據+1的結果進行CAS操作,如果成功就返回結果,負責重試指導成功為止,這里調用compareAndSet是CAS所依賴的JNI的實現的樂觀鎖 。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Atomic就是volatile的使用場景,也是CAS的使用場景。
五、CAS中的ABA問題
CAS使用起來能夠提高性能,但會引起ABA的問題
假如如下事件序列:
1、線程1從內次位置V來獲取值A
2、線程2從內存位置V獲取A
3、線程2進行一些操作,將B寫入到V
4、線程2將A寫入位置V
5、線程1進行CAS操作,發(fā)現位置V的值任然為A,操作成功了
6、線程1盡管CAS操作成功了,該過程有可能出現問題,對于線程1,線程2做的處理就可能丟失了
舉例說明:一個鏈表ABA的例子
1、現有一個用單向鏈表實現的堆棧,棧頂為A。這時線程T1已經知道A.next為B,然后希望用CAS將棧頂替換為B:
1head.compareAndSet(A,B);
2、在T1執(zhí)行上面這條指令之前,線程T2介入,將A、B出棧,再依次入棧D、C、A,而對象B此時處于游離狀態(tài)。
3、此時輪到線程T1執(zhí)行CAS操作,檢測發(fā)現棧頂仍為A,所以CAS成功,棧頂變?yōu)锽。但實際上B.next為null,此時堆棧中只有B一個元素,C和D組成的鏈表不再存在于堆棧中,C、D被丟掉了。

六、ABA問題解決方案
ABA問題解決思路就是使用版本號,在變量前面追加版本號,每次對變量你進行更新的時候對版本進行加1,對于A->B->A 就會變成1A ->2B->3A
七、使用CAS會引起的問題
1.ABA問題
ABA問題可以使用版本號解決
2.循環(huán)時間長開銷大
自旋CAS如果長時間不成功,CPU帶來非常大的執(zhí)行開銷,需要考慮長時間循環(huán)問題,給每個線程循環(huán)給定循環(huán)次數閾值,讓當前線程釋放CPU的使用權,進入阻塞中
3.只能保證一個共享變量的原子操作
八、Synchronized鎖優(yōu)化
JDK1.5之前, Synchronized稱之為“重量級鎖”,對該做了各種所有,分別為偏向鎖、輕量級鎖、重量級鎖
Java對象內存布局:
說到 synchronized 加鎖原理與Java對象在內存中的布局有很大關系, Java 對象內存布局如下:

如上圖所示,在創(chuàng)建一個對象后,在 JVM 虛擬機( HotSpot )中,對象在 Java 內存中的存儲布局 可分為三塊:
對象頭區(qū)域
存放鎖信息,對象年齡等信息
實例數據區(qū)域
此處存儲的是對象真正有效的信息,比如對象中所有字段的內容
對齊填充區(qū)域
JVM 的實現 HostSpot 規(guī)定對象的起始地址必須是 8 字節(jié)的整數倍,換句話來說,現在 64 位的 OS 往外讀取數據的時候一次性讀取 64bit 整數倍的數據,也就是 8 個字節(jié),所以 HotSpot 為了高效讀取對象,就做了"對齊",如果一個對象實際占的內存大小不是 8byte 的整數倍時,就"補位"到 8byte 的整數倍。所以對齊填充區(qū)域的大小不是固定的。
synchronized用的鎖是存在Java對象頭里的,如果對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬等于4字節(jié),即32bit,如下圖:

Java對象頭里的Mark Word里默認存儲對象的HashCode、分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構如下圖所示:

在Java SE 1.6中,鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài),這幾個狀態(tài)會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

九、偏向鎖
偏向鎖的操作根本沒有去找操作系統, 每個對象都有對象頭,看看這個account對象的所謂“對象頭”,其中有個叫做Mark Word:里邊有幾個標識位,還有其他數據。

JVM使用CAS操作把線程ID記錄到了這個Mark Word當中,修改了標識位,當前線程就擁有這把鎖了

可以看出:JVM不用和操作系統協商設置Mutex,它只記錄下線程ID,就表示當前線程擁有這把鎖了,不用操作系統介入
這時線程獲得了鎖,可以執(zhí)行synchronized修飾的代碼塊。
當線程再次執(zhí)行到這個synchronized的時候,JVM通過鎖對象account的Mark Word判斷:“當前線程ID還在,還持有著這個對象的鎖,就可以繼續(xù)進入臨界區(qū)執(zhí)行
這就是偏向鎖,在沒有別的線程競爭的時候,一直偏向當前線程,當前線程可以一直執(zhí)行
十、輕量級鎖
繼續(xù)沿著偏向鎖思路研究
另一個線程0x3704也要進入這個代碼塊執(zhí)行,但是鎖對象account 保存的是當前線程ID,他是沒法進入臨界區(qū)的。
這時也不需要和操作系統交流,JVM可以對偏向鎖升級一下,變成一個輕量級的鎖。
JVM把鎖對象account恢復成無鎖狀態(tài),在當前兩線程的棧幀中各自分配了一個空間,叫做Lock Record,把鎖對象account的Mark Word在倆線程的棧幀中各自復制了一份,叫做Displaced Mark Word
然后當前線程的Lock Record的地址使用CAS放到了Mark Word當中,并且把鎖標志位改為00, 這意味著當前線程也已經獲得了這個輕量級的鎖了,可以繼續(xù)進入臨界區(qū)執(zhí)行。

0x3704線程沒有獲得鎖,但不阻塞,JVM讓他自旋幾次,等待一會兒。等當前退出臨界區(qū),釋放鎖的時候,需要把這個Displaced markd word 使用CAS復制回去。接下來他就可以加鎖了。
兩線程交替著進入臨界區(qū),執(zhí)行這段代碼,相安無事,很少出現真正的競爭。
即使是出現了競爭,想獲得鎖的線程只要自旋幾次,等待一會兒,鎖就可能釋放了。
很明顯,如果沒有競爭或者輕度的競爭,輕量級鎖僅僅使用CAS操作和Lock record就避免了重量級互斥鎖的開銷
十一、重量級鎖
再次分析:輕量級鎖運行時,一線程0x3704 正在持有鎖。另一線程自旋了好多次,0x3704還是沒釋放鎖。 這時候JVM考慮自旋次數太多了浪費CPU。接則升級為重量級鎖!
重量級鎖需要操作系統的介入,依賴操作系統底層的Mutex Lock。
JVM創(chuàng)建了一個monitor 對象,把這個對象的地址更新到了Mark word當中。

在持有鎖運行,而另一線程則切換進程狀態(tài)至:阻塞
到此這篇關于詳解Java中的悲觀鎖與樂觀鎖的文章就介紹到這了,更多相關悲觀鎖與樂觀鎖內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
idea配置檢查XML中SQL語法及書寫sql語句智能提示的方法
idea連接了數據庫,也可以執(zhí)行SQL查到數據,但是無法識別sql語句中的表導致沒有提示,下面這篇文章主要給大家介紹了關于idea配置檢查XML中SQL語法及書寫sql語句智能提示的相關資料,需要的朋友可以參考下2023-03-03
springboot用thymeleaf模板的paginate分頁完整代碼
本文根據一個簡單的user表為例,展示 springboot集成mybatis,再到前端分頁完整代碼,需要的朋友可以參考下2017-07-07

