寫在前面
相信很多小伙伴對于CAS,Synchronized相關的問題都很糾結,包括UP也是,對于這幾個的區(qū)別也困擾了很久,那么今天就來做一個總結,面試再也不用擔心了。
一、 CAS
何為CAS?
CAS(Compare And Swap )是樂觀鎖的一種實現(xiàn)方式,是一種輕量級鎖。JAVA1.5開始引入了CAS,JUC下很多工具類都是基于CAS。
CAS的實現(xiàn)方式:
CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。當多個線程同時嘗試使用CAS更新一個變量時,任何時候只有一個線程可以更新成功,若更新失敗,線程會重新進入循環(huán)再次進行嘗試。

CAS在Java中的應用:
前面也說了JUC下面很多工具類都用到了CAS。其主要依賴于Unsafe的CAS操作來進行實現(xiàn)。
例如AtomicInteger下的incrementAndGet操作:
接著來看看Unsafe下的getAndAddInt方法:
可以看到Unsafe中在循環(huán)體內先讀取內存中的value值,然后CAS更新,如果CAS更新成功則退出,如果更新失敗,則循環(huán)重試直到更新成功。
CAS帶來的問題
ABA問題
例如說:
一. 線程1查詢值是否為A
二. 線程2查詢值是否為A
三. 線程2使用CAS將值更新為B
四. 線程2查詢值是否為B
五. 線程2使用CAS將值更新為A
六. 線程1使用CAS將值更新為C
線程一線程二交替執(zhí)行。第二步到第五步,線程二將值由A更新為B由更新為A,但線程一并沒有察覺,因此線程一還是可以繼續(xù)執(zhí)行。我們稱這種現(xiàn)象為ABA問題。
解決方法:
使用版本號 (時間戳),在每次在執(zhí)行數據的修改操作時,都會帶上一個版本號,一旦版本號和數據的版本號一致就可以執(zhí)行修改操作并對版本號加一,否則就執(zhí)行失敗。例如AtomicStampedReference就是通過對值加一個戳(stamp)來解決“ABA”問題的。
循環(huán)開銷過大
CAS操作不成功的話,會導致一直自旋,CPU的壓力會很大。例如說Unsafe下的getAndAddInt方法會一直循環(huán),知道成功才會返回。
只能保證一個變量的原子操作
二、synchronized
相比于CAS基于樂觀鎖實現(xiàn),synchronized是基于悲觀鎖的,當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。
- 對于普通同步方法加鎖時,鎖是當前實例對象
- 對于靜態(tài)同步方法加鎖時,鎖是當前類的Class對象
- 對于同步方法塊加鎖時,鎖是Synchonized括號里配置的對象
Synchronized的實現(xiàn)方式:
Synchonized是基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,但兩者的實現(xiàn)細節(jié)不一樣。Synchronized 用在方法上時,在字節(jié)碼中是通過方法的 ACC_SYNCHRONIZED 標志來實現(xiàn)的。而代碼塊同步則是使用monitorenter和monitorexit指令實現(xiàn)的。
monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯(lián),當且一個monitor被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖,當獲得對象的monitor以后,monitor內部的計數器就會自增(初始為0),當同一個線程再次獲得monitor的時候,計數器會再次自增。當同一個線程執(zhí)行monitorexit指令的時候,計數器會進行自減,當計數器為0的時候,monitor就會被釋放,其他線程便可以獲得monitor。
Synchronized的優(yōu)化:
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài)。
偏向鎖:
當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖),如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
輕量級鎖:
線程在執(zhí)行同步塊之前,JVM會先在當前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
重量級鎖:
重量級鎖是依賴對象內部的monitor鎖來實現(xiàn)。當系統(tǒng)檢查到鎖是重量級鎖之后,會把等待想要獲得鎖的線程進行阻塞,被阻塞的線程不會消耗cup。但是阻塞或者喚醒一個線程時,都需要操作系統(tǒng)來幫忙,需要從用戶態(tài)轉換到內核態(tài),而轉換狀態(tài)是需要消耗很多時間。
作者::CSDN博主:今天會是有offfer的一天么
原文鏈接:https://blog.csdn.net/HZGuilty/article/details/105630199