參考文章: http://blog.csdn.net/chen77716/article/details/6618779html
目前在Java中存在兩種鎖機制:synchronized和Lock,Lock接口及其實現類是JDK5增長的內容,其做者是大名鼎鼎的併發專家Doug Lea。本文並不比較synchronized與Lock孰優孰劣,只是介紹兩者的實現原理。java
數據同步須要依賴鎖,那鎖的同步又依賴誰?synchronized給出的答案是在軟件層面依賴JVM,而Lock給出的方案是在硬件層面依賴特殊的CPU指令。c++
synrhronized使用普遍。其應用層的語義是能夠把任何一個非null對象做爲"鎖", 當synchronized做用在方法上時,鎖住的即是對象實例(this); 看成用在靜態方法時鎖住的即是對象對應的Class實例,由於Class數據存在於永久帶,所以靜態方法鎖至關於該類的一個全局鎖; 當synchronized做用於某一個對象實例時,鎖住的即是對應的代碼塊。 在HotSpot JVM實現中,鎖有個專門的名字:對象監視器。
當多個線程同時請求某個對象監視器時,對象監視器會設置幾種狀態用來區分請求的線程:
Contention List:全部請求鎖的線程將被首先放置到該競爭隊列
Entry List:Contention List中那些有資格成爲候選人的線程被移到Entry List
Wait Set:那些調用wait方法被阻塞的線程被放置到Wait Set
OnDeck:任什麼時候刻最多隻能有一個線程正在競爭鎖,該線程稱爲OnDeck
Owner:得到鎖的線程稱爲Owner
!Owner:釋放鎖的線程
上圖和文章中提到了ContentionList,又提到了EntryList。windows
還提到了自旋鎖(Spin Lock),在一個線程獲取鎖的時候,先進行自旋,嘗試。雖然對ContentionList中的線程不盡公平,可是效率能夠大大提高。數組
那些處於ContetionList、EntryList、WaitSet中的線程均處於阻塞狀態,阻塞操做由操做系統完成(在Linxu下經過pthread_mutex_lock函數)。
線程被阻塞後便進入內核(Linux)調度狀態,這個會致使系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能
緩解上述問題的辦法即是自旋,其原理是:
當發生爭用時,若Owner線程能在很短的時間內釋放鎖,則那些正在爭用線程能夠稍微等一等(自旋),在Owner線程釋放鎖後,爭用線程可能會當即獲得鎖,
從而避免了系統阻塞。
但Owner運行的時間可能會超出了臨界值,爭用線程自旋一段時間後仍是沒法得到鎖,這時爭用線程則會中止自旋進入阻塞狀態(後退)。
基本思路就是自旋,不成功再阻塞,儘可能下降阻塞的可能性,這對那些執行時間很短的代碼塊來講有很是重要的性能提升。
自旋鎖有個更貼切的名字:自旋-指數後退鎖,也即複合鎖。很顯然,自旋在多處理器上纔有意義。
自旋鎖詳細介紹:緩存
還有個問題是,線程自旋時作些啥?其實啥都不作,能夠執行幾回for循環,能夠執行幾條空的彙編指令,目的是佔着CPU不放,等待獲取鎖的機會。
因此說,自旋是把雙刃劍,若是旋的時間過長會影響總體性能,時間太短又達不到延遲阻塞的目的。
顯然,自旋的週期選擇顯得很是重要,但這與操做系統、硬件體系、系統的負載等諸多場景相關,很難選擇,
若是選擇不當,不但性能得不到提升,可能還會降低,所以你們廣泛認爲自旋鎖不具備擴展性。 對自旋鎖週期的選擇上,HotSpot認爲最佳時間應是一個線程上下文切換的時間,但目前並無作到。
通過調查,目前只是經過彙編暫停了幾個CPU週期,除了自旋週期選擇,HotSpot還進行許多其餘的自旋優化策略,
具體以下:
若是平均負載小於CPUs則一直自旋 若是有超過(CPUs/2)個線程正在自旋,則後來線程直接阻塞 若是正在自旋的線程發現Owner發生了變化則延遲自旋時間(自旋計數)或進入阻塞 若是CPU處於節電模式則中止自旋 自旋時間的最壞狀況是CPU的存儲延遲(CPU A存儲了一個數據,到CPU B得知這個數據直接的時間差) 自旋時會適當放棄線程優先級之間的差別
那synchronized實現什麼時候使用了自旋鎖?
答案是在線程進入ContentionList時,也即第一步操做前。
線程在進入等待隊列時首先進行自旋嘗試得到鎖,若是不成功再進入等待隊列。這對那些已經在等待隊列中的線程來講,稍微顯得不公平。
還有一個不公平的地方是自旋線程可能會搶佔了Ready線程的鎖。自旋鎖由每一個監視對象維護,每一個監視對象一個。
偏向鎖(Biased Lock)主要解決無競爭下的鎖性能問題.安全
首先咱們看下無競爭下鎖存在什麼問題: 如今幾乎全部的鎖都是可重入的,也即已經得到鎖的線程能夠屢次鎖住/解鎖監視對象,
按照以前的HotSpot設計,每次加鎖/解鎖都會涉及到一些CAS操做(好比對等待隊列的CAS操做),CAS操做會延遲本地調用,
所以偏向鎖的想法是一旦線程第一次得到了監視對象,以後讓監視對象「偏向」這個線程,以後的屢次調用則能夠避免CAS操做,
說白了就是置個變量,若是發現爲true則無需再走各類加鎖/解鎖流程。
以上內容來自 http://blog.csdn.net/chen77716/article/details/6618779 可是沒怎麼讀懂 多線程
在搜索偏向鎖的過程當中,又找到下面這篇併發
http://blog.163.com/silver9886@126/blog/static/35971862201472274958280/函數
Java偏向鎖(Biased Locking)是Java6引入的一項多線程優化。它經過消除資源無競爭狀況下的同步原語,進一步提升了程序的運行性能。 偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,若是在接下來的運行過程當中,該鎖沒有被其餘的線程訪問,則持有偏向鎖的線程將永遠不須要觸發同步。 若是在運行過程當中,遇到了其餘線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM會嘗試消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。
(偏向鎖只能在單線程下起做用) 所以 流程是這樣的 偏向鎖->輕量級鎖->重量級鎖
其中還提到輕量級鎖和重量級鎖。那麼這還涉及鎖膨脹。
經過知乎上的一篇回答 https://www.zhihu.com/question/39009953?sort=created
輕量級鎖就是爲了在無多線程競爭的環境中使用CAS來代替mutex,一旦發生競爭,兩條以上線程爭用一個鎖就會膨脹。
回到前一篇
鎖存在Java對象頭裏。若是對象是數組類型,則虛擬機用3個Word(字寬)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。
在32位虛擬機中,一字寬等於四字節,即32bit。
鎖狀態包括:輕量級鎖定、重量級鎖定、GC標記、可偏向
下面兩張圖能夠先忽略,由於後面有更清楚的
(忽略圖,看後面的)32位JVM的Mark Word的默認存儲結構以下:
(忽略圖,看後面的)64位JVM下, Mark Word是64bit大小的,存儲結構以下:
簡單的加鎖機制:
機制:每一個鎖都關聯一個請求計數器和一個佔有他的線程,當請求計數器爲0時,這個鎖能夠被認爲是unhled的,
當一個線程請求一個unheld的鎖時,JVM記錄鎖的擁有者,並把鎖的請求計數加1,若是同一個線程再次請求這個鎖時,請求計數器就會增長,
當該線程退出syncronized塊時,計數器減1,當計數器爲0時,鎖被釋放(這就保證了鎖是可重入的,不會發生死鎖的狀況)。
偏向鎖流程:
偏向鎖,簡單的講,就是在鎖對象的對象頭中有個ThreaddId字段,這個字段若是是空的,
第一次獲取鎖的時候,就將自身的ThreadId寫入到鎖的ThreadId字段內,將鎖頭內的是否偏向鎖的狀態位置1.
這樣下次獲取鎖的時候,直接檢查ThreadId是否和自身線程Id一致,若是一致,則認爲當前線程已經獲取了鎖,所以不需再次獲取鎖,
略過了輕量級鎖和重量級鎖的加鎖階段。提升了效率。
可是偏向鎖也有一個問題,就是當鎖有競爭關係的時候,須要解除偏向鎖,使鎖進入競爭的狀態。
下面是清晰的流程:
上圖中只講了偏向鎖的釋放,其實還涉及偏向鎖的搶佔,其實就是兩個進程對鎖的搶佔,在synchrnized鎖下表現爲輕量鎖方式進行搶佔。
注:也就是說一旦偏向鎖衝突,雙方都會升級爲輕量級鎖。(這一點與輕量級->重量級鎖不一樣,那時候失敗一方直接升級,成功一方在釋放時候notify,加下文後面詳細描述)
以下圖。以後會進入到輕量級鎖階段,兩個線程進入鎖競爭狀態(注,我理解仍然會遵照先來後到原則;注2,的確是的,下圖中提到了mark word中的lock record指向堆棧中最近的一個線程的lock record),一個具體例子能夠參考synchronized鎖機制。(圖後面有介紹)
上面163的文章中,提到了這一篇 http://xly1981.iteye.com/blog/1766224,裏面對於synchronized的過程講的挺好:
每個線程在準備獲取共享資源時:
第一步,檢查MarkWord裏面是否是放的本身的ThreadId ,若是是,表示當前線程是處於 「偏向鎖」
第二步,若是MarkWord不是本身的ThreadId,鎖升級,這時候,用CAS來執行切換,新的線程根據MarkWord裏面現有的ThreadId,通知以前線程暫停,
以前線程將Markword的內容置爲空。
第三步,兩個線程都把對象的HashCode複製到本身新建的用於存儲鎖的記錄空間,接着開始經過CAS操做,
把共享對象的MarKword的內容修改成本身新建的記錄空間的地址的方式競爭MarkWord,
第四步,第三步中成功執行CAS的得到資源,失敗的則進入自旋
第五步,自旋的線程在自旋過程當中,成功得到資源(即以前獲的資源的線程執行完成並釋放了共享資源),則整個狀態依然處於 輕量級鎖的狀態,若是自旋失敗
第六步,進入重量級鎖的狀態,這個時候,自旋的線程進行阻塞,等待以前線程執行完成並喚醒本身
發現,這篇文章對於synchronized講得比較清楚 http://www.infoq.com/cn/articles/java-se-16-synchronized
先介紹一下CAS:
Compare and Swap
比較並設置。用於在硬件層面上提供原子性操做。在 Intel 處理器中,比較並交換經過指令cmpxchg實現。
比較是否和給定的數值一致,若是一致則修改,不一致則不修改。
Java中的每個對象均可以做爲鎖。
當一個線程試圖訪問同步代碼塊時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖。那麼鎖存在哪裏呢?鎖裏面會存儲什麼信息呢?
JVM規範規定JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但二者的實現細節不同。
代碼塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另一種方式實現的,細節在JVM規範裏並無詳細說明,可是方法的同步一樣可使用這兩個指令來實現。
monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處, JVM要保證每一個monitorenter必須有對應的monitorexit與之配對。
任何對象都有一個 monitor 與之關聯,當且一個monitor 被持有後,它將處於鎖定狀態。線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 monitor 的全部權,即嘗試得到對象的鎖。
鎖存在Java對象頭裏。若是對象是數組類型,則虛擬機用3個Word(字寬)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,一字寬等於四字節,即32bit。(下面這個表格講的很清楚)
長度 |
內容 |
說明 |
32/64bit |
Mark Word |
存儲對象的hashCode或鎖信息等。 |
32/64bit |
Class Metadata Address |
存儲到對象類型數據的指針 |
32/64bit |
Array length |
數組的長度(若是當前對象是數組)
|
Java對象頭裏的Mark Word裏默認存儲對象的HashCode,分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構以下:
|
25 bit |
4bit |
1bit 是不是偏向鎖 |
2bit 鎖標誌位 |
無鎖狀態 |
對象的hashCode |
對象分代年齡 |
0 |
01 |
在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。Mark Word可能變化爲存儲如下4種數據:
鎖狀態 |
25 bit |
4bit |
1bit |
2bit |
||
23bit |
2bit |
是不是偏向鎖 |
鎖標誌位 |
|||
輕量級鎖 |
指向棧中鎖記錄的指針 |
00 |
||||
重量級鎖 |
指向互斥量(重量級鎖)的指針 |
10 |
||||
GC標記 |
空 |
11 |
||||
偏向鎖 |
線程ID |
Epoch |
對象分代年齡 |
1 |
01 |
上圖裏面的GC標記,爲11的話,推斷應該是準備GC的意思。
在64位虛擬機下,Mark Word是64bit大小的,其存儲結構以下:
鎖狀態 |
25bit |
31bit |
1bit |
4bit |
1bit |
2bit |
|
|
|
cms_free |
分代年齡 |
偏向鎖 |
鎖標誌位 |
||
無鎖 |
unused |
hashCode |
|
|
0 |
01 |
|
偏向鎖 |
ThreadID(54bit) Epoch(2bit) |
|
|
1 |
01 |
Java SE1.6爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,
因此在Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭狀況逐漸升級。
鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。
這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率,下文會詳細分析。
Hotspot的做者通過以往的研究發現大多數狀況下鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入了偏向鎖。
當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,
之後該線程在進入和退出同步塊時不須要花費CAS操做來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖,
若是測試成功,表示線程已經得到了鎖,若是測試失敗,則須要再測試下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖),若是沒有設置,
則使用CAS競爭鎖,若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。
偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),
它會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否活着,
若是線程不處於活動狀態,則將對象頭設置成無鎖狀態,
若是線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,
棧中的鎖記錄和對象頭的Mark Word要麼從新偏向於其餘線程,要麼恢復到無鎖或者標記對象不適合做爲偏向鎖,最後喚醒暫停的線程。
上面的意思是,先暫停持有偏向鎖的線程,嘗試直接切換。若是不成功,就繼續運行,而且標記對象不適合偏向鎖,鎖膨脹(鎖升級)。
詳見,上面有張圖中的「偏向鎖搶佔模式」:
其中提到了mark word中的lock record指向堆棧最近的一個線程的lock record,其實就是按照先來後到模式進行了輕量級的加鎖。
上文提到全局安全點:在這個時間點上沒有字節碼正在執行。
關閉偏向鎖:偏向鎖在Java 6和Java 7裏是默認啓用的,可是它在應用程序啓動幾秒鐘以後才激活, 若有必要可使用JVM參數來關閉延遲-XX:BiasedLockingStartupDelay = 0。 若是你肯定本身應用程序裏全部的鎖一般狀況下處於競爭狀態,能夠經過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那麼默認會進入輕量級鎖狀態。
輕量級鎖加鎖:線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。
而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
輕量級鎖解鎖:輕量級解鎖時,會使用原子的CAS操做來將Displaced Mark Word替換回到對象頭,若是成功,則表示沒有競爭發生。
若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。
注:輕量級鎖會一直保持,喚醒老是發生在輕量級鎖解鎖的時候,由於加鎖的時候已經成功CAS操做;而CAS失敗的線程,會當即鎖膨脹,並阻塞等待喚醒。(詳見下圖)
下圖是兩個線程同時爭奪鎖,致使鎖膨脹的流程圖。
鎖不會降級
由於自旋會消耗CPU,爲了不無用的自旋(好比得到鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。
當鎖處於這個狀態下,其餘線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖以後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。
鎖 |
優勢 |
缺點 |
適用場景 |
偏向鎖 |
加鎖和解鎖不須要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 |
若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 |
適用於只有一個線程訪問同步塊場景。 |
輕量級鎖 |
競爭的線程不會阻塞,提升了程序的響應速度。 |
若是始終得不到鎖競爭的線程使用自旋會消耗CPU。 |
追求響應時間。 同步塊執行速度很是快。 |
重量級鎖 |
線程競爭不使用自旋,不會消耗CPU。 |
線程阻塞,響應時間緩慢。 |
追求吞吐量。 同步塊執行速度較長。 |
上面這張表格好好看,總結的很是好!
對象頭源碼markOop.hpp。偏向鎖源碼biasedLocking.cpp。以及其餘源碼ObjectMonitor.cpp和BasicLock.cpp。
上面這篇文章(http://www.infoq.com/cn/articles/java-se-16-synchronized)講得很是清晰了。下面還有幾篇文章以前打開了,看是否有補充。
http://blog.csdn.net/wolegequdidiao/article/details/45116141
這篇文章提到:
輕量級鎖加鎖進行的CAS操做中,是先更新Lock Record指針,而後再更新最後2bit的鎖標記位(也不必定,沒有明確說。可是各類鎖的順序要一致;注:涉及偏向鎖,極可能是先改鎖標記位的)。
下面這張圖,和上面那張圖是一致的:
輕量級鎖能提升程序同步性能的依據是「對於絕大部分的鎖,在整個同步週期內都是不存在競爭的」,這是一個經驗數據。若是沒有競爭,輕量級鎖使用CAS操做避免了使用互斥量的開銷,但若是存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操做,所以在有競爭的狀況下,輕量級鎖會比傳統的重量級鎖更慢。
JDK1.6引入
注意:偏向鎖的鎖標記位和無鎖是同樣的,都是01,可是有單獨一位偏向標記設置是否偏向鎖。
再複習一下,輕量級鎖00,重量級鎖10,GC標記11,無鎖 01.
下面這張圖作一個複習:
偏向鎖能夠提升帶有同步但無競爭的程序性能。若是程序中大多數的鎖老是被多個不一樣的線程訪問,那偏向模式就是多餘的。
在具體情形分析下,禁止偏向鎖優反而可能提高性能。
看這篇文章的一些筆記 http://www.cnblogs.com/javaminer/p/3889023.html
在jdk1.6中對鎖的實現引入了大量的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、
偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減小鎖操做的開銷。
鎖粗化(Lock Coarsening):也就是減小沒必要要的緊連在一塊兒的unlock,lock操做,將多個連續的鎖擴展成一個範圍更大的鎖。
鎖消除(Lock Elimination):經過運行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊之外被其餘線程共享的數據的鎖保護,
經過逃逸分析也能夠在線程本地Stack上進行對象空間的分配(同時還能夠減小Heap上的垃圾收集開銷)。
輕量級鎖(Lightweight Locking):這種鎖實現的背後基於這樣一種假設,即在真實的狀況下咱們程序中的大部分同步代碼通常都處於無鎖競爭狀態
(即單線程執行環境),在無鎖競爭的狀況下徹底能夠避免調用操做系統層面的重量級互斥鎖,
取而代之的是在monitorenter和monitorexit中只須要依靠一條CAS原子指令就能夠完成鎖的獲取及釋放。
當存在鎖競爭的狀況下,執行CAS指令失敗的線程將調用操做系統互斥鎖進入到阻塞狀態,當鎖被釋放的時候被喚醒(具體處理步驟下面詳細討論)。
偏向鎖(Biased Locking):是爲了在無鎖競爭的狀況下避免在鎖獲取過程當中執行沒必要要的CAS原子指令,
由於CAS原子指令雖然相對於重量級鎖來講開銷比較小但仍是存在很是可觀的本地延遲(可參考這篇文章)。
適應性自旋(Adaptive Spinning):當線程在獲取輕量級鎖的過程當中執行CAS操做失敗時,在進入與monitor相關聯的操做系統重量級鎖
(mutex semaphore)前會進入忙等待(Spinning)而後再次嘗試,當嘗試必定的次數後若是仍然沒有成功則調用與該monitor關聯的semaphore(即互斥鎖),
進入到阻塞狀態。
注:(適應性)自旋鎖,是在從輕量級鎖向重量級鎖膨脹的過程當中使用的,是在進入重量級鎖以前進行的。
輕量級鎖具體實現: 一個線程可以經過兩種方式鎖住一個對象:一、經過膨脹一個處於無鎖狀態(狀態位001)的對象得到該對象的鎖;
2、對象已經處於膨脹狀態(狀態位00)但LockWord指向的monitor record的Owner字段爲NULL,
則能夠直接經過CAS原子指令嘗試將Owner設置爲本身的標識來得到鎖。 從中能夠看出,是先檢查鎖的標識位。
看下面這篇文章的記錄:
http://www.cnblogs.com/javaminer/p/3892288.html?utm_source=tuicool&utm_medium=referral
偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令。
其餘,這篇文章也沒什麼須要注意的內容了。
這時候,我忽然想到,爲何CAS就不能對標識位和數據一塊兒操做呢,一次操做完成是否能夠呢?
CAS應用
CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。
下面從分析比較經常使用的CPU(intel x86)來解釋CAS的實現原理。 下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼: public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
能夠看到這是個本地方法調用。這個本地方法在openjdk中依次調用的c++代碼爲:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。
對於32位/64位的操做應該是原子的:
奔騰6和最新的處理器能自動保證單處理器對同一個緩存行裏進行16/32/64位的操做是原子的,可是複雜的內存操做處理器不能自動保證其原子性,
好比跨總線寬度,跨多個緩存行,跨頁表的訪問。可是處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操做的原子性。
CAS的缺點
CAS雖然很高效的解決原子操做,可是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操做 1. ABA問題。由於CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,
那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。ABA問題的解決思路就是使用版本號。
在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。 從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。
這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,
則以原子方式將該引用和該標誌的值設置爲給定的更新值。 關於ABA問題參考文檔: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html 2. 循環時間長開銷大。自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。若是JVM能支持處理器提供的pause指令那麼效率會有必定的提高,
pause指令有兩個做用,第一它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,
延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。
第二它能夠避免在退出循環的時候因內存順序衝突(memory order violation)而引發CPU流水線被清空(CPU pipeline flush),從而提升CPU的執行效率。 3. 只能保證一個共享變量的原子操做。當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,
可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖,
或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。
從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。
AtomicReference類的學習能夠參考如下
http://www.cnblogs.com/skywang12345/p/3514623.html
簡單源碼示例以下(已經實際實驗驗證):
// AtomicReferenceTest.java的源碼 import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceTest { public static void main(String[] args){ // 建立兩個Person對象,它們的id分別是101和102。 Person p1 = new Person(101); Person p2 = new Person(102); // 新建AtomicReference對象,初始化它的值爲p1對象 AtomicReference ar = new AtomicReference(p1); // 經過CAS設置ar。若是ar的值爲p1的話,則將其設置爲p2。 ar.compareAndSet(p1, p2); Person p3 = (Person)ar.get(); System.out.println("p3 is "+p3); System.out.println("p3.equals(p1)="+p3.equals(p1)); } } class Person { volatile long id; public Person(long id) { this.id = id; } public String toString() { return "id:"+id; } }
運行結果:
p3 is id:102 p3.equals(p1)=false
以上,是關於synchronized, 偏向鎖,輕量級鎖,重量級鎖,自旋鎖,CAS等的一些內容和筆記。
後續還會再對JVM以及Java的一些機制和實現作更多學習。
好比會新開一篇關於JVM的文章,