程式設計師:不能逃避的synchronize和volatile

Java貓說發表於2019-09-24

本部落格 貓叔的部落格,轉載請申明出處

閱讀本文約 “10分鐘”

適讀人群:Java 初級

學習筆記,我也是呆呆做了好久,學了一下PS,然後繼續思考了一會,再開始寫出來的,希望可以簡明易懂。

原子性

首先是我們彼此都要保持一致的觀點:原子(Atomic)操作指相應的操作是單一不可分割的操作

emmmm,這裡很牽強的解釋下原子性,還是不懂就搜搜其他文章,最好看看一些具體的例子

首先是程式碼例子

對int型變數conut執行counter++的操作不是原子操作

這可以分為3個操作

  • 1、讀取變數counter的當前值
  • 2、拿counter當前值和1做加法運算
  • 3、將counter的當前值增加1後賦值給counter變數

上面的步驟2,很有可能在執行的時候就已經被其他執行緒修改了,其所為的“當前值”已經是過期的

或者看看百度百科的例子

我們以decl (遞減指令)為例,這是一個典型的"讀-改-寫"過程,涉及兩次記憶體訪問。設想在不同CPU執行的兩個程式都在遞減某個計數值,可能發生的情況是:

  • ⒈ CPU A(CPU A上所執行的程式,以下同)從記憶體單元把當前計數值⑵裝載進它的暫存器中;
  • ⒉ CPU B從記憶體單元把當前計數值⑵裝載進它的暫存器中。
  • ⒊ CPU A在它的暫存器中將計數值遞減為1;
  • ⒋ CPU B在它的暫存器中將計數值遞減為1;
  • ⒌ CPU A把修改後的計數值⑴寫回記憶體單元。
  • ⒍ CPU B把修改後的計數值⑴寫回記憶體單元。

記憶體裡的計數值應該是0,然而它卻是1。兩個程式都去掉了對該共享資源的引用,但沒有一個程式能夠釋放它--兩個程式都推斷出:計數值是1,共享資源仍然在被使用

我再舉例我呆想到的例子,一個姐姐和一個妹妹一起包餃子

image

畫的很一般,別看我這樣,我也是學過2小時速成素描的·····

假設我們在一個黑盒環境下,就是兩姐妹都在各自小空間包餃子,然後她們把餃子通過各自的小洞口放入一個大盒子裡。她們並不知道對方(比如她們兩剛剛因為媽媽不給零花錢而生氣了)

這個時候她們各自同時邊賭氣邊包了一個餃子,同時放到盒子裡,媽媽跑過來問老大,盒子裡有多少個了?她只知道一個。再問問老二,她也是回答一個。這個生活例子可能提交特殊,不過偶爾生活中因為資訊不對稱而導致的預知結果與實際有偏差也是經常發生的

所以他們腦海就是這個情況。其實盒子裡已經是2個餃子了

image

那麼其實這個場景也像是JVM

image

synchronize

synchronize關鍵字可以實現操作的原子性,其本質是通過該關鍵字所包括的臨界區的排他性保證在任何一個時刻只有一個執行緒能夠執行臨界區中的程式碼

也就是說,現在媽媽說只有聽她的,兩姐妹才能有零花錢,所以她叫兩個鬧脾氣的小鬼都到廚房,並拿出了大盒子,讓她們重新開始,不過要按照媽媽的要求來

image

媽媽先讓姐姐包了5個,因為兩姐妹都在廚房,不是各自在房間,所以這次妹妹都看在眼裡,接著媽媽讓妹妹包10個,妹妹顯然是有點不樂意了(憑什麼我姐才5個),不過她還是老實做了,現在他們三人都知道盒子裡有15個

這裡就又牽出了synchronize的另一個特點,保證記憶體的可見性

它保證了一個執行緒執行臨界區中的程式碼時所修改的變數值對於稍有執行該臨界區中的程式碼的執行緒來說是可見的,這對於保證多執行緒的程式碼是非常重要的

官方的解釋下:CPU執行程式碼,為了減少變數訪問的消耗,會將值快取到CPU快取區,再次訪問的時候,就是從快取區去讀取而不是主記憶體,這裡的快取區有點類似姐姐腦海/妹妹腦海。而且程式碼對快取區的修改可能僅修改快取區,沒有被寫回主記憶體。由於CPU都有自己的儲存區,對於不同CPU的儲存區內容是不可見的。這也是所謂的記憶體可見性

volatile

同樣這個兄弟也可以保證記憶體可見性

一個執行緒對於一個採用volatile修改的變數的值的更改對於其他訪問該變數的值的執行緒總是可見的

如果說對比synchronize和volatile的記憶體鎖,然後說volatile是輕量級鎖,emmmm,不好不太恰當

volatile的內部鎖並不能保證操作的原子性。

他在記憶體可見性的核心機制是:修改的值會被寫入主記憶體,且其他CPU快取區的值會因此失效(然後再更新一個最新值),保證其他執行緒訪問volatile修飾的變數總是最新值。

當然他也有一個核心作用:禁止指令重排序(Re-order)

你們一般怎麼寫5的?

image

假如以上是我們的規定與希望

可能編譯器和CPU為了提供指令的執行效率可能會進行指令重排序(優化)

image

如果你希望它是按照規定來的話就加上volatile,雖然可能會導致編譯器和CPU無法對一些指令做可能的優化,假設上面那樣寫對於計算機來說算優化:)

用程式來寫一個例子:

private SomeOne object = new SomeOne();
複製程式碼

你先想一下,你覺得的順序,好了,我說說計算機可能的順序

  • 1、分配一段用於儲存SomeOne的記憶體空間
  • 2、對該記憶體空間引用賦值給變數object
  • 3、建立類SomeOne

如果當其他執行緒訪問2、object變數的時候,僅得到一個指向儲存SomeOne儲存空間的引用,因為3、SomeOne還沒建立

結語

希望各位兄弟能看到一些新的風景,synchronize可以保證操作原子性,且保證記憶體可見性;volatile僅能保證記憶體可見性。

synchronize會導致上下文切換,volatile不會哦。

關於上下文切換的,可以去看公眾號的上一篇文章

我是MySelf,還在堅持學習技術與產品經理相關的知識,希望本文能給你帶來新的知識點。

公眾號:Java貓說

學習交流群:728698035

現架構設計(碼農)兼創業技術顧問,不羈平庸,熱愛開源,雜談程式人生與不定期乾貨。

Image Text

相關文章