程序員:不能逃避的synchronize和volatile

本博客 貓叔的博客,轉載請申明出處

閱讀本文約 “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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章