volatile與Atomic的比較

dave160947發表於2020-11-12

 

 

 

一、volatile的作用(防止重排序,可見性),不能保證原子性

我們已經知道可見性、有序性及原子性問題,通常情況下我們可以通過Synchronized關鍵字來解決這些個問題,不過如果對Synchronized原理有了解的話,應該知道Synchronized是一個比較重量級的操作,對系統的效能有比較大的影響,所以,如果有其他解決方案,我們通常都避免使用Synchronized來解決問題。

而volatile關鍵字就是Java中提供的另一種解決可見性和有序性問題的方案。對於原子性,需要強調一點,也是大家容易誤解的一點:對volatile變數的單次讀/寫操作可以保證原子性的,如long和double型別變數,但是並不能保證i++這種操作的原子性,因為本質上i++是讀、寫兩次操作。

二、volatile的使用

關於volatile的使用,我們可以通過幾個例子來說明其使用方式和場景。

1、防止重排序

現在我們分析一下為什麼要在變數上volatile關鍵字。要理解這個問題,先要了解物件的構造過程,例項化一個物件其實可以分為三個步驟:

  • 分配記憶體空間。

  • 初始化物件。

  • 將記憶體空間的地址賦值給對應的引用。

但是由於作業系統可以對指令進行重排序,所以上面的過程也可能會變成如下過程:

  • 分配記憶體空間。

  • 將記憶體空間的地址賦值給對應的引用。

  • 初始化物件

如果是這個流程,多執行緒環境下就可能將一個未初始化的物件引用暴露出來,從而導致不可預料的結果。因此,為了防止這個過程的重排序,我們需要將變數設定為volatile型別的變數。

2、實現可見性

可見性問題主要指一個執行緒修改了共享變數值,而另一個執行緒卻看不到。引起可見性問題的主要原因是每個執行緒擁有自己的一個快取記憶體區——執行緒工作記憶體。

JMM記憶體模型的可見性,指的是當主記憶體區域中的值被某個執行緒寫入更改後,其它執行緒會馬上知曉更改後的值,並重新得到更改後的值。

總體上來說volatile的理解還是比較困難的,如果不是特別理解,也不用急,完全理解需要一個過程,在後續的文章中也還會多次看到volatile的使用場景。這裡暫且對volatile的基礎知識和原來有一個基本的瞭解。

總體來說,volatile是併發程式設計中的一種優化,在某些場景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的場景下,才能適用volatile。總的來說,必須同時滿足下面兩個條件才能保證在併發環境的執行緒安全:

  • 對變數的寫操作不依賴於當前值。

  • 該變數沒有包含在具有其他變數的不變式中。

3.為什麼不能保證原子性

簡單的說,修改volatile變數分為四步:

1)讀取volatile變數到local

2)修改變數值

3)local值寫回

4)插入記憶體屏障,即lock指令,讓其他執行緒可見

這樣就很容易看出來,前三步都是不安全的,取值和寫回之間,不能保證沒有其他執行緒修改。原子性需要鎖來保證。

這也就是為什麼,volatile只用來保證變數可見性,但不保證原子性

 

  Atomic(效能高,輕量),存在ABA問題可以通過新增版本號解決

 

 

 

優點:

  按理來說,使用synchroized已經能滿足功能需求了。為什麼還會有這個類呢?那肯定是效能的問題了。

  在JDK1.6之前,synchroized是重量級鎖,即操作被鎖的變數前就對物件加鎖,不管此物件會不會產生資源競爭。這屬於悲觀鎖的一種實現方式。

  而CAS會比較記憶體中物件和當前物件的值是否相同,相同的話才會更新記憶體中的值,不同的話便會返回失敗。這是樂觀鎖的一中實現方式。這種方式就避免了直接使用核心狀態的重量級鎖。

  但是在JDK1.6以後,synchronized進行了優化,引入了偏向鎖,輕量級鎖,其中也採用了CAS這種思想,效率有了很大的提升。

Atomic類的缺點

  ABA問題:

    對於一箇舊的變數值A,執行緒2將A的值改成B又改成可A,此時執行緒1通過CAS看到A並沒有變化,但實際A已經發生了變化,這就是ABA問題。解決這個問題的方法很簡單,記錄一下變數的版本就可以了

  自旋問題:

    atomic類會多次嘗試CAS操作直至成功或失敗,這個過程叫做自旋。通過自旋的過程我們可以看出自旋操作不會將執行緒掛起,從而避免了核心執行緒切換,但是自旋的過程也可以看做CPU死迴圈,會一直佔用CPU資源。

 

相關文章