併發編程系列之volatile和synchronized實現原理

前言

上節我們講了併發的一些挑戰,算是開啓併發編程的大門,今天我們就來說說併發中最基本的兩個東西volatile和Synchronized的底層實現原理,我們都知道Java代碼在編譯後會變成字節碼,然後被類加載器加載到JVM中,JVM執行字節碼,最終需要轉化爲彙編指令在CPU上去執行,因此Java中所使用的的併發機制是依賴於JVM的實現和CPU的指令完成的。ok,那麼我們現在就開啓我們今天的併發之旅吧。

 

Synchronized

Synchronized是併發編程中最基本最古老的元素,一般也被稱爲重量級鎖

 

Synchronized實現同步的基礎

Java中每一個對象都可以作爲鎖,具體表現爲下面3種形式:

  • 對於普通同步方法,鎖是當前實例對象

  • 對於靜態同步方法,鎖是當前類的class對象

  • 對於同步方法塊,鎖是synchronized包含的代碼塊

 

Synchronized在JVM中的實現原理

JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,synchronized關鍵字在經過編譯之後,會在同步塊的前後形成monitorenter和monitorexit這;兩個字節碼指令,這兩個字節碼都需要一個引用類型的參數來指明要鎖定和解鎖的對象,如果synchronized明確指定了對象參數,那就是這個對象的引用,如果沒有明確指定,那就根據synchronized修飾的是實例方法還是類方法,去取對應對象的實例或者class對象來作爲鎖的對象。

 

monitorenter:執行monitorenter時,首先要嘗試獲取對象的鎖,如果這個對象沒被鎖定,或者當前線程已經擁有了該對象的鎖,那麼就把鎖的計數器+1;

monitorexit:執行monitorexit的時候就會將鎖的計數器-1,當計數器爲0時,鎖就被釋放,如果一個線程獲取鎖失敗,那麼就會阻塞等待,直到對象鎖被釋放爲止;

對於monitorenter和monitorexit行爲描述中有2點需要注意:

  • synchronized同步塊對同一個線程來說是可重入的,不會出現自己把自己鎖死的問題

  • 同步塊在已進入的線程執行完之前,會阻塞後續線程的進入,也就是說同步塊只允許一個線程正在執行

 

synchronized使用的鎖存在哪?

synchronized用的鎖存放在Java對象頭裏面的,對象頭存儲結構如下:

Java對象頭分爲2部分信息,第一部分存儲對象自身運行數據(哈希碼,GC分代年齡等)官方稱Mark Word,第二部分用於存儲指向方法區對象類型數據的指針,如果對象爲數組,額外存儲個數組的長度;

Mark Word默認存儲對象的hashcode,分代年齡和鎖標記位,在32位JVM和64位JVM下存儲是不同的,32位下大小爲32bit,64位下大小爲64bit,分別如下所示:

在運行期間,Mark Word中存儲的數據會隨着鎖標誌位變化發生改變,如下圖所示:

 

鎖的升級和對比

引入偏向鎖和輕量級鎖的目的是爲了減少獲得鎖和釋放鎖帶來的性能開銷,鎖一共有4種狀態,級別由低到高:無鎖狀態、偏向鎖狀態·輕量級鎖狀態和重量級鎖狀態,這幾個狀態隨着競爭情況逐漸升級,但是不可以降級,也就是說偏向鎖升級爲輕量級鎖之後,不能降級爲偏向鎖,這種策略也是爲了提高鎖的獲取和釋放效率。

 

偏向鎖

偏向鎖的獲取:

偏向鎖的釋放:使用一種等到競爭出現才釋放鎖的機制,當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,過程如下:

偏向鎖的關閉:偏向鎖在1.6和1.7中默認是啓用的,但是在應用程序啓動後會有幾秒鐘的延遲才能激活,通過JVM參數-XX:BiasedLockingStartupDelay=0來設置關閉延遲激活,如果你想要徹底關閉偏向鎖,可以使用-XX:-UseBiasedLocking=false,那麼程序將禁止使用偏向鎖,默認直接進入輕量級鎖狀態。

 

輕量級鎖

輕量級鎖獲取:

輕量級鎖釋放:

因爲自旋會消耗CPU,所以鎖一旦升級爲重量級鎖,就不會再降級到輕量級鎖,這也就是前面提到的鎖升級策略的原因。

 

對比

 

volatile

volatile是輕量級的synchronized,它在多處理器環境下保證了共享變量的“可見性”(當一個線程修改一個共享變量時,這個變量值得修改,對於其他線程都是可見的,即其他線程都能正確的獲取到修改後的值),volatile由於不存在線程上下文切換和調度,所以一般情況下比synchronized的執行成本要低,那麼接下來我們就看下,處理器是如何實現volatile的。

 

volatile的定義

Java語言規範中指出:Java編程語言允許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量

Java提供的volatile,在某些情況下會比鎖更加的好用,Java線程內存模型確保所有線程看到的變量(volatile聲明)都是一致的。

 

volatile是如何保證可見性的

首先我們瞭解下,被volatile修飾的變量進行寫操作時JVM是怎麼操作的:JVM會向處理器發送一條帶有Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作時,就會有問題,所以,在多處理器下,爲了保證各個處理器的緩存一致性,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期的,當處理器發現自己緩存的行對應的內存地址被修改,就會將當期處理器的緩存設置爲無效狀態,當處理器對這個數據進行修改時,會重新從系統內存中把最新數據讀到處理器緩存中;

 

從上述過程我們可以得到,volatile進行寫操作時,CPU會對收到一條帶有Lock前綴的彙編指令,該指令主要處理以下兩件事情:

  • Lock前綴指令會引起處理器緩存回寫到內存

  • 一個處理器的緩存回寫到內存會導致其他處理器的緩存無效

 

可以理解爲,volatile修飾的變量有一份本地緩存的數據,當變量被其他線程修改時,主內存就會通知各個本地緩存的數據,並將其設置爲無效的,當該線程再操作本地緩存數據時,發現數據失效,就會強制去主內存獲取最新數據,並寫回本地緩存,狀態改爲有效,從而保證了變量的獲取總是取最新的值,也就是說變量的修改對於所有線程都是可見的。

 

今天的內容就到這,主要是介紹併發中synchronized和volatile併發機制的底層實現原理,後期還會從別的角度談synchronized和volatile的具體應用和細節

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