原來java有這么多把鎖,圖解java中的17把鎖
樂觀鎖和悲觀鎖
獨占鎖和共享鎖
互斥鎖和讀寫鎖
公平鎖和非公平鎖
可重入鎖
自旋鎖
分段鎖
鎖升級(無鎖|偏向鎖|輕量級鎖|重量級鎖)
鎖優化技術(鎖粗化、鎖消除)
1、悲觀鎖
悲觀鎖對應于生活中悲觀的人,悲觀的人總是想著事情往壞的方向發展。
舉個生活中的例子,假設廁所只有一個坑位了,小明上廁所會第一時間把門反鎖上,這樣其他人上廁所只能在門外等候,這種狀態就是「阻塞」了
在數據中因為總是假設最壞的情況,所以每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖,有任何一線程對數據進行讀寫操作都會上鎖,數據庫的行鎖、表鎖、讀鎖,寫鎖都是悲觀鎖,Java中synchronized鎖和ReentrantLock 就是悲觀鎖思想的實現。
關于ReentrantLock鎖,具體的實現原理請看我的另一篇文章:ReentrantLock底層原理、手寫Lock鎖
2、樂觀鎖
樂觀鎖?對應于生活中樂觀的人,樂觀的人總是想著事情往好的方向發展。
樂觀鎖總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量,有任何一線程對數據進行寫操作都會上鎖;也就是讀的時候不上鎖,寫的時候才上鎖,
樂觀鎖一般用cas和版本號機制實現,如果想要了解具體原理請看我的另一篇文章 :?CAS 自旋鎖/無鎖機制算法
Java.util.concurrent.atomic 包下的原子類就是使用CAS 樂觀鎖實現的
樂觀和悲觀兩種鎖的使用場景
悲觀鎖和樂觀鎖沒有孰優孰劣,有其各自適應的場景。
樂觀鎖適用于寫比較少(沖突比較小)的場景,因為不用上鎖、釋放鎖,省去了鎖的開銷,從而提升了吞吐量。
如果是寫多讀少的場景,即沖突比較嚴重,線程間競爭激勵,使用樂觀鎖就是導致線程不斷進行重試,這樣可能還降低了性能,這種場景下使用悲觀鎖就比較合適。
3、獨占鎖
獨占鎖是指鎖一次只能被一個線程所持有。如果一個線程對數據加上排他鎖后,那么其他線程不能再對該數據加任何類型的鎖。獲得獨占鎖的線程即能讀數據又能修改數據。
JDK中的synchronized和java.util.concurrent(JUC)包中Lock的實現類就是獨占鎖
悲觀鎖和獨占鎖的區別
悲觀鎖是針對讀寫上鎖,而獨占鎖是針對線程的獨占,這一點需要區分,但其本質是一樣的,有人說獨占鎖就是悲觀鎖,這樣講也可以;
4、共享鎖
共享鎖是指鎖可被多個線程所持有。如果一個線程對數據加上共享鎖后,那么其他線程只能對數據再加共享鎖,不能加獨占鎖。獲得共享鎖的線程只能讀數據,不能修改數據。
舉個現實中的例子,就拿小區樓房里面的電梯來說,電梯是大家共享使用的,任何一個人都不能把電梯據為己有,更不能對電梯做改裝操作,大家都只有使用權,而沒有改裝權
在 JDK 中?ReentrantReadWriteLock?就是一種共享鎖。
5、互斥鎖
互斥鎖是獨占鎖的一種常規實現,是指某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。
互斥鎖一次只能有一個線程擁有互斥鎖,其他線程只能等待,和悲觀鎖和獨占鎖類似
6、讀寫鎖
為了保證多個線程同時訪問一個資源進行讀寫操作時不會出現臟讀的情況,有必要使用讀寫鎖進行限制,多個線程同時讀一個資源時沒有任何問題,但是讀寫同時進行時就會有問題,寫寫操作也會有問題,為了保證線程安全的情況下,就需要一個讀寫鎖來解決這個問題;
接下來我們用2個線程模擬四種情況,讓我們看看底層有哪些變化,其中AB代表2個不同的線程去讀取同一個資源;
1、兩個線程都讀取數據
2、先讀后寫
3、先寫后讀
4、兩個線程都寫
在 JDK 中定義了一個讀寫鎖的接口:ReadWriteLock,而ReentrantReadWriteLock?實現了ReadWriteLock接口;
7、公平鎖
公平鎖指的是多個線程按照指定的順序執行,整個過程有序地進行每個線程,就像銀行排隊一樣,需要辦理業務的人們排成一排,有序地進行辦理業務,上一個人業務辦理完成后進行下一個人的業務辦理!
優點:所有的線程都能得到資源,不會餓死在隊列中。
缺點:吞吐量會下降很多,隊列里面除了第一個線程,其他的線程都會阻塞,cpu喚醒阻塞線程的開銷會很大
在 java 中可以通過構造函數初始化公平鎖
/**
* 創建一個可重入鎖,true 表示公平鎖,false 表示非公平鎖。默認非公平鎖
*/
Lock lock = new ReentrantLock(true);
8、非公平鎖
非公平鎖顧名思義,就是不公平的,多個線程通過搶占的方式進行執行,誰搶到就給誰執行,就跟強盜邏輯是一樣的,誰搶到就是誰的;一旦有一個線程搶到鎖資源之后,其他的線程只能進入阻塞狀態,等待下一輪的搶占;
優點:可以減少CPU喚醒線程的開銷,整體的吞吐效率會高點,CPU也不必取喚醒所有線程,會減少喚起線程的數量。
缺點:你們可能也發現了,這樣可能導致隊列中間的線程一直獲取不到鎖或者長時間獲取不到鎖,導致餓死!
在 java 中 synchronized 關鍵字是非公平鎖,ReentrantLock默認也是非公平鎖。
9、可重入鎖/遞歸鎖
重入鎖指的是在同一線程,在外層函數獲得鎖之后,內層遞歸函數仍然有獲得鎖的代碼,但執行時不受影響,比如A方法加鎖了,B方法也加鎖了,A調用B方法,表面上看是有2個鎖,但實際上他們用的都是同一把鎖!
synchronized 和 ReentrantLock 都擁有可重入鎖的特性,它們都是可重入鎖
10、自旋鎖
自旋鎖是指線程在沒有獲得鎖時不是被直接掛起,而是執行一個忙循環,這個忙循環會一直判斷是否有可用的鎖資源,如果有可用的鎖資源,就會通過比較和交換(CAS)的方式去搶占資源,如果搶不到則進入下一個忙循環,一直到搶到鎖資源為止,這就是所謂的自旋。
自旋鎖的目的是為了減少線程被掛起的幾率,因為線程的掛起和喚醒也都是耗資源的操作。
如果鎖被另一個線程占用的時間比較長,即使自旋了之后當前線程還是會被掛起,忙循環就會變成浪費系統資源的操作,反而降低了整體性能。因此自旋鎖是不適應鎖占用時間長的并發情況的。
在 Java 1.5之后的并發包中,Atomic開頭的類都具有自旋的操作,底層是用CAS(比較和交換)實現的
自適應自旋
CAS 操作如果失敗就會一直循環獲取當前 value 值然后重試。本質上自旋也是一種開銷,所以在JDK1.6又引入了自適應自旋,這個就比較智能了,自旋時間不再固定,由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。如果虛擬機認為這次自旋也很有可能再次成功那就會次序較多的時間,如果自旋很少成功,那以后可能就直接省略掉自旋過程,避免浪費處理器資源。
11、分段鎖
分段鎖設計目的是將鎖的粒度進一步細化,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作
在 Java 語言中 CurrentHashMap 底層就用了分段鎖,使用Segment,就可以進行并發使用了。
12、鎖升級(無鎖|偏向鎖|輕量級鎖|重量級鎖)
在jdk1.6之前,synchronized 是一把很重的鎖,每次加鎖的時候都會向操作系統申請鎖資源,但是有時候我們在只有一個線程的情況下并不需要這么重的鎖,因此每次都用這么重的鎖會帶來很大的消耗;所以在jdk1.6開始,為了提升性能減少獲得鎖和釋放鎖所帶來的消耗,引入了4種鎖的狀態:無鎖、偏向鎖、輕量級鎖和重量級鎖,它會隨著多線程的競爭情況逐漸升級,但不能降級。
關于鎖升級的所有原理請看我另一篇文章里有詳細介紹,這里不過多贅述:synchronized 鎖升級過程
鎖優化技術(鎖粗化、鎖消除)
13、鎖粗化
鎖粗化就是將多個同步塊的數量減少,并將單個同步塊的作用范圍擴大,本質上就是將多次上鎖、解鎖的請求合并為一次同步請求。
舉個例子,一個循環體中有一個代碼同步塊,每次循環都會執行加鎖解鎖操作
private static final Object LOCK = new Object();
for(int i = 0;i < 100; i++) {
synchronized(LOCK){
// do some magic things
}
}
經過鎖粗化后就變成下面這個樣子了:
synchronized(LOCK){
for(int i = 0;i < 100; i++) {
// do some magic things
}
}
14、鎖消除
鎖消除是指虛擬機編譯器在運行時檢測到了共享數據沒有競爭的鎖,從而將這些鎖進行消除。
舉個例子讓大家更好理解。
public String test(String s1, String s2){
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
return stringBuffer.toString();
}
上面代碼中有一個 test 方法,主要作用是將字符串 s1 和字符串 s2 串聯起來。
test 方法中三個變量s1, s2, stringBuffer, 它們都是局部變量,局部變量是在棧上的,棧是線程私有的,所以就算有多個線程訪問 test 方法也是線程安全的。
我們都知道 StringBuffer 是線程安全的類,append 方法是同步方法,但是 test 方法本來就是線程安全的,為了提升效率,虛擬機幫我們消除了這些同步鎖,這個過程就被稱為鎖消除。
StringBuffer.class,消除后的append方法如下,去掉了?synchronized 關鍵字;
// append 是同步方法
public StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
一張圖總結
目前位置,java中所有的鎖都在這里面了!
Java JDK 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。