面試官超級喜歡問的CAS
文末本文轉載自微信公眾號「程序員巴士」,作者tech-bus.七十一 。轉載本文請聯系程序員巴士公眾號。
前言
自學了一年JAVA阿巴阿巴終于約到了面試,這次面試官讓她談談對CAS的理解。
回去等通知
如果對CAS完全不了解的同學建議先去看看相關的博客了解了基本的原理,再來看面試的時候如何解答
面試官: 對CAS有了解嗎?可以講講嗎?
阿巴阿巴: 了解一些,CAS全稱Compare And Swap,也就是比較和交換。
阿巴阿巴: CAS的思想比較簡單,主要涉及到三個值:當前內存值V、預期值(舊的內存值)O、即將更新的內存值U,當且僅當預期值O與當前內存值V相等時,將內存值V修改為更新值U,并返回true,否則返回false。
面試官: 還有嘛?CAS的使用場景知道嗎?
阿巴阿巴: 額...應該差不多了,CAS好像在并發包里使用到了。
面試官: 好,CAS有啥缺點嗎?
阿巴阿巴: 額....好..好像有個ABA的問題,好像是用AtomicStampedReference解決。
面試官: 還有其他缺點嗎?
阿巴阿巴: 額...記不太清了....
面試官: 行,那你這邊先回去等通知哈??
阿巴阿巴: 好的~
當場發offer
面試官: CAS了解嗎?講講
阿巴阿巴: CAS全稱Compare and Swap,也就是比較和交換。
阿巴阿巴: CAS的思想比較簡單,主要涉及到三個值:當前內存值V、預期值(舊的內存值)O、即將更新的內存值U,當且僅當預期值O與當前內存值V相等時,將內存值V修改為更新值U,返回true,否則返回false。
阿巴阿巴: CAS主要使用在一些需要上鎖的場景充當樂觀鎖解決方案,一般在一些簡單且要上鎖的操作但又不想引入鎖場景,這時候來使用CAS代替鎖。
阿巴阿巴: CAS主要涉及到三個問題:ABA問題、自旋帶來的消耗、CAS只能單變量
面試官: 可以詳細講一下這三個問題嗎?
阿巴阿巴: ABA問題是指有一個線程t1在進行CAS操作時,其他線程t2將變量A改成了B,然后又將其改成A,這時候t1發現A并沒有改變,因此進行了交換操作,由于在交換操作進行前變量A其實是有變化的,只不過最終又修改回A了,此A非彼A,這時候進行交換操作在一些業務場景下很可能要出問題,要解決ABA問題有2種方案。
阿巴阿巴: 方案一:在對變量進行操作的時候給變量加一個版本號,每次對變量操作都將版本號加1,常見在數據庫的樂觀鎖中可見。
阿巴阿巴: 方案二:Java提供了相應的原子引用類AtomicStampedReference,它通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題。
阿巴阿巴: 自旋帶來的消耗CAS自旋如果很長時間都不成功,這會給CPU帶來很大的開銷
阿巴阿巴: 解決方案:1、代碼層面破壞掉for循環,設置合適的循環次數。2、使用JVM能支持處理器提供的pause指令來提升效率,它可以延遲流水線執行指令,避免消耗過多CPU資源。
阿巴阿巴: CAS只能單變量對于一個共享變量,可以使用CAS方式來保證原子操作,但是當多個共享變量時,那就無法使用CAS來保證原子性。JDK1.5開始,提供了AtomicReference類來保證引用對象之前的原子性,就可以把多個變量放在一個對象里來進行CAS操作。
阿巴阿巴: 在JDK1.5中新增的java.util.concurrent(JUC),就是建立在CAS之上的,一般來說CAS這種樂觀鎖適合讀多寫少的場景。
面試官見阿巴阿巴對答如流,決定為難一下她。
面試官: 了解JMM嗎,講一下JMM。
阿巴阿巴: 知道一些,JMM是JAVA內存模型(JAVA Memory Model),目的是為了屏蔽各種硬件和操作系統之間的內存訪問差異,從而讓JAVA程序在各種平臺對內存的訪問一致。
阿巴阿巴: 不僅如此,JMM還規定了所有的變量都存儲在主存中,每個線程都有自己獨立的工作空間,線程對變量的操作必須先從主存中讀取到自己的工作內存中然后再進行操作,最后回寫回主存。
阿巴阿巴: 關于主存和工作內存的交互JAVA定義了八種操作來完成,且這些操作都是原子性的:lock、unlock、read、load、use、assign、store、write。
面試官: 不錯不錯,那JMM是真實存在的嘛,和JVM內存模型(JAVA 虛擬機內存模型)是一樣的嘛?
阿巴阿巴: 不是真實存在的,JMM講的也只是一種模型,真實的實現可能還是和模型會有差異的。JMM和JVM是不一樣的,它們并不是同一個層次的劃分,基本上沒啥關系。
堆和方法區是線程共享的,虛擬機棧、本地方法棧、程序計數器是線程私有的。
程序計數器是這幾塊區域唯一一個不會發生OOM的區域。
面試官: 理解的還不錯嘛,那你講講Volatile關鍵字唄。
阿巴阿巴: Volatile可以說是JAVA虛擬機提供的最輕量級的同步機制,當一個變量被定義為volatile后,它將具備倆種特性,第一個是保證此變量對所有線程的可見性,即當一個線程改變了這個變量的值后,其他線程能夠立即感知的到,雖然具有可見性,但是多線程在并發情況下對volatile修飾的變量進行操作時是會有線程安全性的問題的。這是因為volatile修飾的變量在各個線程工作內存中是不存在一致性的,但是由于每次使用都要進行刷新,導致執行引擎看不到不一致的情況。
阿巴阿巴: Volatile修飾的變量的第二個特性是禁止指令重排序優化,普通的變量僅僅會保證在該方法的執行過程中所有依賴的賦值結果的地方都能夠獲取到正確的結果。而不能保證賦值的順序和代碼中的書寫順序一致。例如下面的DCL的單例模式。
- public class instance {
- private String str = "";
- private volatile static instance ins = null;
- /**
- * 構造方法私有化
- */
- private instance(){
- str = "hi";
- }
- /**
- * DCL獲取單例
- * @return
- */
- public static instance getinstance(){
- if (ins == null){
- synchronized (instance.class){
- if (ins == null){
- ins = new instance();
- }
- }
- }
- return ins;
- }
- }
阿巴阿巴: 如果上面ins變量不使用volatile變量進行修飾,那么當線程A在獲取了instance.class鎖后,對ins變量進行 ins = new instance() 初始化時,由于這是很多條指令,jvm可能會亂序執行。這個時候如果線程B在執行if (ins == null)時,正常情況下,如果為true,說明需要獲取instance.class鎖,等待初始化。但是這時候,假設線程A再沒有對ins進行初始化完,比如只分配了空間,對象還沒構造完,但是已經將引用返回了,這樣線程B得到的就是一個未能實例化完全的對象,從而發生異常。而加了volatile關鍵字后,如果實例還未初始化完成,那么它的引用是不會向外發布的,這樣即可避免異常的發生。
面試官: 不錯,你這塊都掌握的挺扎實的,明天可以來上班了。
阿巴阿巴: 好的??