Java併發編程之驗證volatile不能保證原子性

Java併發編程之驗證volatile不能保證原子性

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

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

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

原子性操作的定義:

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

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

模擬場景:

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

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

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

Java併發編程之驗證volatile不能保證原子性

再來看看主線程裡面:

Java併發編程之驗證volatile不能保證原子性

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

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

Java併發編程之驗證volatile不能保證原子性

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

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

我們來分析:

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

Java併發編程之驗證volatile不能保證原子性

說明:

主內存中有共享變量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指令重排-理論篇》


分享到:


相關文章: