volatile、synchronized、final原理淺析

1. 前言

只會使用,不明白原理,就不能靈活運用,深入理解這幾個關鍵字,對於併發編程來講頗有幫助。html

2. volatile

2.1 volatile 的做用

volatile 的做用有一下兩點:編程

  • 修改當即可見
  • 禁止指令重排序

可見性是指當一個線程修改了共享變量的值,其它線程可以適時得知這個修改。segmentfault

可見性

致使線程可見性問題有兩個緣由:多線程

  1. 線程對變量進行修改未同步到主內存,那麼這個線程對改變量的修改就是不可見的。
  2. 重排序。爲了提升程序的執行效率,編譯器在生成指令序列時和CPU執行指令序列時,都有可能對指令進行重排序。

2.2 原理

實現可見性: 禁用工做內存和 Happens-Before 規則的前三條併發

實現可見性

由 JMM 可知通常的變量寫是先寫到工做內存,而後由 buffer 刷到主存中的。volatile 標記的變量禁用了工做內存,即直接寫到主存中。其餘線程讀取該變量時,直接從主內存中讀取。app

Happens-Before 規則的前三條:函數

  1. 程序順序規則:一個線程中的每一個操做,happens-before於該線程中的任意後續操做。
  2. 監視器鎖規則:對一個鎖的解鎖,happens-before 於隨後對這個鎖的加鎖。
  3. volatile變量規則:對一個 volatile 域的寫,happens-before於任意後續對這個volatile 域的讀。

Happens-Before 規則可參考:post

Java內存模型以及happens-before規則優化

禁止重排序的實現其實也依賴了 happen-before 原則。操作系統

JVM底層是經過一個叫作「內存屏障」的東西來完成。內存屏障,也叫作內存柵欄,是一組處理器指令,用於實現對內存操做的順序限制。

屏障類型 指令示例 說明
LoadLoad Barriers Load1;LoadLoad;Load2 該屏障確保Load1數據的裝載先於Load2及其後全部裝載指令的的操做
StoreStore Barriers Store1;StoreStore;Store2 該屏障確保Store1馬上刷新數據到內存(使其對其餘處理器可見)的操做先於Store2及其後全部存儲指令的操做
LoadStore Barriers Load1;LoadStore;Store2 確保Load1的數據裝載先於Store2及其後全部的存儲指令刷新數據到內存的操做
StoreLoad Barriers Store1;StoreLoad;Load2 該屏障確保Store1馬上刷新數據到內存的操做先於Load2及其後全部裝載裝載指令的操做。它會使該屏障以前的全部內存訪問指令(存儲指令和訪問指令)完成以後,才執行該屏障以後的內存訪問指令

基於保守策略的 JMM 內存屏障插入策略:

  • 在每一個 volatile 寫操做的前面插入一個 StoreStore 屏障。該屏障用來保證在 volatile 寫以前,其前面全部的普通寫操做已經對任意處理器可見
  • 在每一個 volatile 寫操做的後面插入一個 StoreLoad 屏障。避免 volatile 寫操做可能與後面可能有的 volatile 寫操做重排序
  • 在每一個 volatile 讀操做的前面插入一個 LoadLoad 屏障。用來避免將 volatile 讀和前面的普通讀重排序
  • 在每一個 volatile 讀後面插入一個 LoadStore 屏障。避免後面的普通寫和 volatile 讀重排序

所謂的保守策略即保證在任何處理器上都能獲得正確的語意,實際上編譯器會自動優化以省略某些語意(好比由於 X86 不會對讀讀、讀寫、寫寫重排序,就能夠省下這三種屏障)

編譯器不會對 volatile 讀與 volatile 讀後面的任意內存操做(包括對普通變量的讀寫)重排序,也不會對 volatile 寫與 volatile 寫前面的任意內存操做重排序

2.3 總結

簡而言之,volatile 變量自身具備下列特性:

  1. 可見性。對一個 volatile 變量的讀,總能看到(任意線程)對這個 volatile 變量最後的寫入。
  2. 原子性。對任意單個 volatile 變量的讀/寫具備原子性,但相似於 volatile++ 這種複合操做不具備原子性。

3. final

final 修飾變量、修飾方法、修飾類,都有什麼做用就不詳細講解了,講講原理。

對於final域,編譯器和處理器要遵照兩個重排序規則:

  • 在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。(先寫入final變量,後調用該對象引用)

緣由:編譯器會在final域的寫以後,插入一個StoreStore屏障

  • 初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序(先讀對象的引用,後讀final變量)

緣由:編譯器會在讀final域操做的前面插入一個LoadLoad屏障

詳細講解參考:

Java併發(十九):final實現原理

4. synchronized

synchronized 的底層是使用操做系統的 mutex lock 實現的。

  • **內存可見性:**同步快的可見性是由「若是對一個變量執行 lock 操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前須要從新執行 load 或 assign 操做初始化變量的值」、「對一個變量執行 unlock 操做以前,必須先把此變量同步回主內存中(執行 store 和 write 操做)」這兩條規則得到的。
  • **操做原子性:**持有同一個鎖的兩個同步塊只能串行地進入

synchronized 用的鎖是存在 Java 對象頭裏的。

JVM基於進入和退出 Monitor 對象來實現方法同步和代碼塊同步。代碼塊同步是使用 monitorentermonitorexit 指令實現的,monitorenter 指令是在編譯後插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結束處和異常處。任何對象都有一個 monitor 與之關聯,當且一個 monitor 被持有後,它將處於鎖定狀態。`

根據虛擬機規範的要求,在執行 monitorenter 指令時,首先要去嘗試獲取對象的鎖,若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1;相應地,在執行 monitorexit 指令時會將鎖計數器減1,當計數器被減到 0 時,鎖就釋放了。若是獲取對象鎖失敗了,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。

注意兩點:

一、synchronized 同步快對同一條線程來講是可重入的,不會出現本身把本身鎖死的問題;

二、同步塊在已進入的線程執行完以前,會阻塞後面其餘線程的進入。

想要詳細瞭解,下面這篇文章講德特別棒:

Java synchronized原理總結

5. 小結&參考資料

小結

這三個關鍵字在 JMM 中起着相當重要的做用,JMM 規範的保證依靠這些關鍵字實現。同時,這幾個關鍵字的熟練使用也很是很是重要,得深入理解。

參考資料

Java 併發編程:volatile的使用及其原理

Java併發編程的藝術

從Java多線程可見性談Happens-Before原則

談亂序執行和內存屏障

Java final關鍵字及其內存語義

Final of Java,這一篇差很少了

Java併發(十九):final實現原理

Java synchronized原理總結

相關文章
相關標籤/搜索