Java併發程式設計之驗證volatile不能保證原子性

通過系列文章的學習,凱哥已經介紹了volatile的三大特性。1:保證可見性 2:不保證原子性 3:保證順序。那麼怎麼來驗證可見性呢?本文凱哥(凱哥Java:kaigejava)將通過程式碼演示來證明為什麼說volatile不能夠保證共享變數的原子性操作。

我們來舉個現實生活中的例子:

中午去食堂打飯,假設你非常非常的飢餓,需要一葷兩素再加一份米飯。如果食堂打飯的阿姨再給你打一個菜的時候,被其他人打斷了,給其他人打飯,然後再回過頭給你打飯。你選一葷兩素再加一份米飯打完的過程被打斷了四次耗時30分鐘。你想想你自己的感受。是不是要瘋了,要暴走了!其實,如果把從你點菜到阿姨給你打完飯這個過程,看著計算機的一個執行緒執行過程的話,那麼在你點菜到你拿到飯菜這個過程是一個完整的,不能被打斷的,這就是所謂的原子性。如果被多次打斷的話想想你的心理,就知道程式如果在執行過程被打斷後的結果了。

原子性操作的定義:

所謂的原子性操作就是執行緒對變數的操作一旦開始,就會一直執行直到結束。中介不會因為其他原因而切換到另一個執行緒。操作是不可分割的,在執行完畢之前是不會被其他任務或是事件中斷的。一個操作或者是多個操作要麼執行都成功要麼執行都失敗(可以結合資料庫的原子性理解)。

怎麼證明volatile修飾的共享變數就不能保證原子性呢?

模擬場景:

共享變數volatile int number=0;執行number++操作。使用多個執行緒多次呼叫。看看使用volatile修飾的number在執行結束後的結果是否是我們預期的結果。

我們分別用10個執行緒執行100次,50個執行緒執行1000次以及50個執行緒執行一百萬次來看看結果。

先來看看變數是用volatil修飾的

再來看看主執行緒裡面:

按照上面咱們規定的執行緒數量執行次數來看看咱們預期結果和實際執行結果:

我們分別用10個執行緒執行100次,50個執行緒執行1000次以及50個執行緒執行一百萬次來

 

執行緒數量

執行次數

number預期結果

實際執行結果

10

100

10*100=1000

1000

50

1000

五萬

49297

200

1000

二十萬

194181

50

1000000

5千萬

7246921

 

 

從上面表格中我們可以看到,即時共享變數用volatile修飾了。但是隨著執行緒數量或者執行次數的增加,實際執行結果與預期結果相差越來越大。如果預期結果和執行結果一致則說明保證了原子性,但是從結果來看不是這樣的。從而證明了volatile的第二個特性:不能保證原子性。

為什麼從i++的執行結果上就能看出不保證原子性呢?

我們來分析:

正常來說200個執行緒,每個執行緒執行了1000次。最後應該輸出的是:200*1000=20000.二十萬。但是實際結果卻不是二十萬次。那說明了什麼呢?請看下圖:

 

說明:

主記憶體中有共享變數number的值是0,現在有4個CPU帶著4個執行緒都從主記憶體中copy變數到自己的工作區。這個是CPU1先競爭到然後再執行緒1的工作區中執行了number++.執行後將number的值更新成了1,寫回到主記憶體中了。這個時候正要或者正在通知其他CPU主記憶體中的number值變化了。CPU2和CPU3都收到通知了,將自己工作區的變數置為無效,重新從主記憶體獲取到number=1的值。這個時候CPU4執行的也快,在還沒有收到CPU1的通知的時候,就將自己執行後的number++的值也寫回到了主記憶體中。其實這個時候,cpu1執行緒1的操作還在進行中,但是因為cpu4執行緒4的操作打斷了執行緒1的操作。第一輪執行結果應該是4,但是因為執行緒4把執行緒1執行打斷了,將執行緒1執行結果覆蓋了。所以實際執行後的效果有可能是3或者2但是不可能是4.

從上分析結果,我們更能理解到volatile修飾的共享變數不能保證原子性了。因為有可能被其他執行緒打斷執行。

怎麼解決原子性問題呢?可以使用juc包下的atomic包下的物件就可以了。

Volatile的有序性證明,歡迎學習下一篇:《Java併發程式設計之驗證volatile指令重排-理論篇》

歡迎關注凱哥公眾號:凱哥Java(kaigejava)

相關文章