面試必考:Java執行緒同步之Syncronized關鍵字

面試必考:Java執行緒同步之Syncronized關鍵字

前言

之前我們透過volatiile關鍵字修飾一個變數i,可以使得在A執行緒中修改這個變數的副本之後能夠立即重新整理回記憶體,並且其他執行緒在讀取之前必須要從主記憶體中讀取這個值,從而達到了簡單的執行緒間同步資料的目的

但是我們也看到這個機制並不是完整的,當訪問的變數值依賴於這個變數的前值的時候,這個機制會失效,也就是volatile只解決變數的記憶體可見性,但是並沒有解決原子性。

今天就來看看一個最常用的解決執行緒間同步的機制以及其背後的原理。

問題

繼續我們之前的程式碼

面試必考:Java執行緒同步之Syncronized關鍵字

當多個執行緒訪問PlainSeller的例項時,ticket最終的值並不等於increment的呼叫次數,也就是說如果有10個執行緒,每個執行緒呼叫了一次increment,按理說ticket的值應該是10,但是結果中有時候會出現比10小的情況。

解決

面試必考:Java執行緒同步之Syncronized關鍵字

只需要在increment方法前面加上synchronized關鍵字即可,我們大白話一下這裡發生了什麼。

當CPU在執行指令的時候,當遇到了synchronized修飾的方法時會去全域性找一把鑰匙,如果這個鑰匙能夠拿到,則能夠繼續執行,如果不能拿到,則需要等待,這樣就確保了在同一個時刻只有一個執行緒在執行方法體中的指令,也就不會產生同時訪問一個變數導致的主記憶體與執行緒本地快取中值不一致的現象了。

如果方法中指令很多,但只有很小一部分程式碼在同時訪問臨界資源的時候,這個時候在方法上加synchronized就會很影響效率,因為不需要同步的指令也需要等待拿到鑰匙之後才能執行,這個時候我們可以在程式碼塊上加synchronized關鍵字,如下:

面試必考:Java執行緒同步之Syncronized關鍵字

原理

首先看下兩個increment的JVM指令:

SyncronizedSeller

面試必考:Java執行緒同步之Syncronized關鍵字

PlainSeller

面試必考:Java執行緒同步之Syncronized關鍵字

它倆唯一的區別就是flags上增加了ACC_SYNCHRONIZED位,區別並不明顯,看不出來有什麼差異。

我們再來看看對應的彙編指令,貼出synchronized部分指令如下:

面試必考:Java執行緒同步之Syncronized關鍵字

這裡跟不加synchronized關鍵字的區別在於多了很多 lock cmpxchg 指令。

那我們看一看這個指令是幹啥的。

先看cmpxchg,這個指令是比較然後交換的意思,那麼比較什麼交換什麼呢?簡單說,多執行緒中有個叫CAS的原理或者演算法,透過這個演算法,可以讓兩個執行緒同步的訪問同一個變數從而達到消除上述bug的作用,在x86指令集裡面,CAS原理就是透過cmpxchg這條指令實現的,並且在多核CPU上這條指令需要加上lock字首,所以就有了lock cmpxchg指令,syncronized透過這條指令的實現,從而達到了同步訪問變數的目的。

那麼這裡就留下了一個坑:CAS原理。

小結

今天我們透過synchronized關鍵字解決了多個執行緒同時訪問臨界資源的時候帶來的記憶體原子性問題,我們也透過背後的JVM指令和彙編指令瞭解其背後的原理,接下來我們繼續看看還有哪些方法能夠解決記憶體原子性問題。如果大家可以給透過crazy042438這個v號問我吧