synchronized實現同步的基礎:Java中的每一個對象都可以作為鎖。具體表現為以下3種形式。
-
對於普通同步方法,鎖是當前實例對象(this)。
-
對於靜態同步方法,鎖是當前類的Class對象。
-
對於同步方法塊,鎖是synchonized括號里配置的對象。
(重點) synchronized不僅保證了操作的原子性,還保證了操作變量的內存可見性。所以如果僅僅是只考慮內存可見性,synchronized和volatile都能實現,但為了效率,通常是使用volatile。
從JVM規范中可以看到synchonized在JVM里的實現原理,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步;
monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處;
JVM要保證每個monitorenter必須有對應的monitorexit與之配對;
任何對象都有一個monitor與之關聯,當且一個monitor被持有后,它將處於鎖定狀態;
線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖;
同步方法,依靠的 是方法修飾符上的 ACC_SYNCHRONIZED 實現;
synchronized用的鎖是存在Java對象頭里的,如果對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭,在32位虛擬機中,1字寬等於4字節,即32bit。
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”;
在Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級;
鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖后不能降級成偏向鎖,這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
鎖的表示
Java里的鎖,主要都是對對象進行加鎖,如普通的synchronized非靜態方法,就是對當前執行方法的對象進行加鎖。
那么怎么對對象進行加鎖呢?對象的鎖其實主要就是通過對象頭的markOop(官方叫做MarkWord)進行表示的。markOop其實不是一個對象,只是一個字長的數據,在32為機器上,markOop為32個位,在64位上為64個位。
markOop中不同的位區域存儲着不同的信息,但是需要注意的一點是,markOop每個位區域表示的信息不是一定的,在不同狀態下,markWord中存着不同的信息。
下圖很重要,是synchronized鎖狀態的對象頭表示:
理解性記憶對象頭可以從無鎖->偏向->輕量->重量->GC標記 方式對比記憶,對象頭里鎖標志位一次是01->01->00->10->11
1 偏向鎖
很多情況下,一個鎖對象並不會發生被多個線程訪問得情況,更多是被同一個線程進行訪問,如果一個鎖對象每次都被同一個線程訪問,根本沒有發生並發,但是每次都進行加鎖,那豈不是非常耗費性能,所以偏向鎖就被設計出來了;
偏向,也可以理解為偏心;
當鎖對象第一次被某個線程訪問時,它會在其對象頭的markOop中記錄該線程ID,那么下次該線程再次訪問它時,就不需要進行加鎖了;
當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲着指向當前線程的偏向鎖;
如果測試成功,表示線程已經獲得了鎖;如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程(線程ID);
但是這中間只要發生了其他線程訪問該鎖對象的情況,證明這個鎖對象會發生並發,就不能對這個對象再使用偏向鎖了,會進行鎖的升級。
2 輕量級鎖
當有競爭且競爭不強烈時,JVM就會由偏向鎖膨脹為輕量級鎖,考慮到線程的阻塞和喚醒需要CPU從用戶態轉為核心態(增加CPU負擔),而這種轉換對CPU來說是一件負擔很重的操作,因此沒有獲取到鎖的線程不會進入阻塞狀態,而是通過自旋的方式一直嘗試獲取鎖,處於一種忙等狀態,所以這種處理競爭的方式比較浪費CPU,但是響應速度很快。
3 重量級鎖
當競爭激烈時,不會立刻進入阻塞狀態而是會自旋一段時間看是否能獲取鎖如果不能則進入阻塞狀態。
三種鎖的特點
鎖 | 優點 | 缺點 | 使用場景 |
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級別的差距 | 如果線程間存在鎖競爭,會帶類額外的鎖撤銷的消耗 | 適用於只有一個線程訪問同步塊的場景 |
輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度 | 如果始終得不到鎖競爭的線程,使用自旋會消耗CPU | 追求響應時間,同步塊執行速度非常快 |
重量級鎖 | 線程競爭幾乎不使用自旋,不會消耗CPU | 阻塞線程,響應時間緩慢 | 追求吞吐量,同步塊執行速度較長 |
自旋鎖
例子:死循環內sun.misc.Unsafe對象的CAS操作。
來源:
https://blog.51cto.com/wenshengzhu/2062647