從JMM資料原子操作來分析volatile

2021-10-07 11:24:57 字數 3001 閱讀 3942

併發程式設計的三個性質:原子性,可見性,有序性

兩線程同時對i做i++,執行緒1往記憶體寫經過匯流排時被執行緒2感知到了,執行緒2做i++的操作已經做完了,然而因為感知到i變了,做i無效化重新讀,所以執行緒2做的那次i++的操作執行了但是沒起作用

在討論原子性操作時,我們經常會聽到乙個說法:任意單個volatile變數的讀寫具有原子性,但是volatile++這種操作除外。

所以問題就是:為什麼volatile++不是原子性的?

因為它實際上是三個操作組成的乙個符合操作。

結合記憶體屏障這個概念對volatile的讀寫操作深入理解的話:

第一步:讀

在第一步操作的指令後,會增加兩個記憶體屏障:

在volatile讀操作前插入loadload屏障,防止前面的volatile讀與後面的普通讀重排序

在volatile讀操作後插入loadstore屏障,防止前面的volatile讀與後面的普通寫重排序

因此第乙個指令和它後續的普通讀寫操作會被保證沒有重排序來搗亂。通常是去記憶體中去讀。

那麼問題又來了,為什麼通常去記憶體中讀?

其實這個問題要說細的話可以很細,大概就兩個關鍵點吧:

volatile的寫操作的快取失效機制

最後乙個對volatile變數執行寫操作的cpu,由於在它對應的快取中保有最新的值,因此可以不用再去主存裡面獲取

具體看下面第三步的分析。

第二步:自增

這個步驟沒什麼特別的,就是在cpu自身的快取記憶體(暫存器,l1-l3 cache)中完成。不涉及到快取和記憶體的互動。

第三步:寫

volatile寫算是乙個重點。

根據jmm對於volatile變數型別的語義規範:volatile在編譯之後,會在變數寫操作時新增lock字首指令。這個lock字首指令在多核處理器的環境中,有這樣的作用:

通知cpu將當預處理器快取行的資料寫回到系統主存中

該寫回操作將使其他cpu快取了該記憶體位址的資料無效

另外,記憶體屏障在volatile的寫操作中起到了很大的作用,來保證上面兩點能夠實現:

在volatile寫操作前插入storestore屏障,防止前面其他寫與本次volatile寫重排序

在volatile寫操作後插入storeload屏障,防止本次的volatile寫與後面的讀操作重排序

那麼為了解決volatile++這類復合操作的原子性,有什麼方案呢?其實方案也比較多的,這裡提供兩種典型的:

使用synchronized關鍵字

使用atomicinteger/atomiclong原子型別

synchronized是比較原始的同步手段。它本質上是乙個獨佔的,可重入的鎖。當乙個執行緒嘗試獲取它的時候,可能會被阻塞住,所以高併發的場景下效能存在一些問題。

在某些場景下,使用synchronized關鍵字和volatile是等價的:

寫入變數值時候不依賴變數的當前值,或者能夠保證只有乙個執行緒修改變數值。

寫入的變數值不依賴其他變數的參與。

讀取變數值時候不能因為其他原因進行加鎖。

加鎖可以同時保證可見性和原子性,而volatile只保證變數值的可見性。

這類原子型別比鎖更加輕巧,比如atomicinteger/atomiclong分別就代表了整型變數和長整型變數。

在它們的實現中,實際上分別使用的volatile int/volatile long儲存了真正的值。因此,也是通過volatile來保證對於單個變數的讀寫原子性的。

在此基礎之上,它們提供了原子性的自增自減操作。比如incrementandget方法,這類方法相對於synchronized的好處是:它們不會導致執行緒的掛起和重新排程,因為在其內部使用的是cas非阻塞演算法。

cas是什麼

所謂的cas全程為compareandset。直譯過來就是比較並設定。這個操作需要接受三個引數:

記憶體位置

舊的預期值

新值這個操作的做法就是看指定記憶體位置的值符不符合舊的預期值,如果符合的話就將它替換成新值。它對應的是處理器提供的乙個原子性指令 - cmpxchg。

比如atomiclong的自增操作:

public final long incrementandget() 

}public final boolean compareandset(long expect, long update)

我們考慮兩個執行緒t1和t2,同時執行到了上述step 1處,都拿到了current值為1。然後通過step 2之後,current在兩個執行緒中都被設定為2。

緊接著,來到step 3。假設執行緒t1先執行,此時符合compareandset的設定規則,因此記憶體位置對應的值被設定成2,執行緒t1設定成功。當執行緒t2執行的時候,由於它預期current為1,但是實際上已經變成了2,所以compareandset執行不成功,進入到下一輪的for迴圈中,此時拿到最新的current值為2,如果沒有其它執行緒感染的話,再次執行compareandset的時候就能夠通過,current值被更新為3。

所以不難發現,cas的工作主要依賴於兩點:

無限迴圈,需要消耗部分cpu效能

cpu原子指令compareandset

雖然它需要耗費一定的cpu cycle,但是相比鎖而言還是有其優勢,比如它能夠避免執行緒阻塞引起的上下文切換和排程。這兩類操作的量級明顯是不一樣的,cas更輕量一些。

總結我們說對於volatile變數的讀/寫操作是原子性的。因為從記憶體屏障的角度來看,對volatile變數的單純讀寫操作確實沒有任何疑問。

由於其中摻雜了乙個自增的cpu內部操作,就造成這個復合操作不再保有原子性。

臨界資料 臨界區和原子操作

1 首先給出這三個名詞的定義。臨界資料指多個程序 或執行緒 會競爭修改的資料。臨界區指修改臨界資料的 區域。原子操作指臨界區的 不會被這個臨界資料的其他臨界區的 打斷。2 通過乙個例項來理解這些概念。在這個例項中臨界資料是標準輸出,臨界資料對應的其中乙個臨界區就是圖中紅框部分,紅框中的臨界區 不應該...

從硬體操作到核心來談網絡卡驅動

dm9000任務 1 把上層傳下來的資料 sk buff 抽取有效資料通過 dm9000 轉為二進位制傳送出去 2 接收來自其他裝置的二進位制資料,通過 dm9000 得到這些資料,然後封裝為 sk buff 格式,然後提交給上一層。我們需要對網絡卡驅動做的主要任務 1 網絡卡初始化,然網絡卡執行起...

通過Odoo Shell來操作線上的資料

odoo shell 提供了乙個簡便的操作 odoo的互動介面,從 odoo 9.0 開始就是標準功能,無需安裝第三方應用。odoo shell是 通過在 cli command.py commands 註冊 shell command 來實現的。首先,odoo支援的 command 都是基於 co...