【死磕Java並發】-----深入分析volatile的實現原理

來源:互聯網
上載者:User

標籤:表示   分享   ret   重排序   共用變數   運行程式   有序性   java   通知   

通過前面一章我們瞭解了synchronized是一個重量級的鎖,雖然JVM對它做了很多最佳化,而下面介紹的volatile則是輕量級的synchronized。如果一個變數使用volatile,則它比使用synchronized的成本更加低,因為它不會引起線程內容相關的切換和調度。Java語言規範對volatile的定義如下:

Java程式設計語言允許線程訪問共用變數,為了確保共用變數能被準確和一致地更新,線程應該確保通過獨佔鎖定單獨獲得這個變數。

上面比較繞口,通俗點講就是說一個變數如果用volatile修飾了,則Java可以確保所有線程看到這個變數的值是一致的,如果某個線程對volatile修飾的共用變數進行更新,那麼其他線程可以立馬看到這個更新,這就是所謂的線程可見度。

volatile雖然看起來比較簡單,使用起來無非就是在一個變數前面加上volatile即可,但是要用好並不容易(LZ承認我至今仍然使用不好,在使用時仍然是模稜兩可)。

記憶體模型相關概念

理解volatile其實還是有點兒難度的,它與Java的記憶體模型有關,所以在理解volatile之前我們需要先瞭解有關Java記憶體模型的概念,這裡只做初步的介紹,後續LZ會詳細介紹Java記憶體模型。

作業系統語義

電腦在運行程式時,每條指令都是在CPU中執行的,在執行過程中勢必會涉及到資料的讀寫。我們知道程式啟動並執行資料是儲存在主存中,這時就會有一個問題,讀寫主存中的資料沒有CPU中執行指令的速度快,如果任何的互動都需要與主存打交道則會大大影響效率,所以就有了CPU快取。CPU快取為某個CPU專屬,只與在該CPU啟動並執行線程有關。

有了CPU快取雖然解決了效率問題,但是它會帶來一個新的問題:資料一致性。在程式運行中,會將運行所需要的資料複製一份到CPU快取中,在進行運算時CPU不再也主存打交道,而是直接從快取中讀寫資料,只有當運行結束後才會將資料重新整理到主存中。舉一個簡單的例子:

i++

當線程運行這段代碼時,首先會從主存中讀取i( i = 1),然後複製一份到CPU快取中,然後CPU執行 + 1 (2)的操作,然後將資料(2)寫入到告訴緩衝中,最後重新整理到主存中。其實這樣做在單線程中是沒有問題的,有問題的是在多線程中。如下:

假如有兩個線程A、B都執行這個操作(i++),按照我們正常的邏輯思維主存中的i值應該=3,但事實是這樣嗎?分析如下:

兩個線程從主存中讀取i的值(1)到各自的快取中,然後線程A執行+1操作並將結果寫入快取中,最後寫入主存中,此時主存i==2,線程B做同樣的操作,主存中的i仍然=2。所以最終結果為2並不是3。這種現象就是緩衝一致性問題。

解決緩衝一致性方案有兩種:

  1. 通過在匯流排加LOCK#鎖的方式
  2. 通過緩衝一致性協議

但是方案1存在一個問題,它是採用一種獨佔的方式來實現的,即匯流排加LOCK#鎖的話,只能有一個CPU能夠運行,其他CPU都得阻塞,效率較為低下。

第二種方案,緩衝一致性協議(MESI協議)它確保每個緩衝中使用的共用變數的副本是一致的。其核心思想如下:當某個CPU在寫資料時,如果發現操作的變數是共用變數,則會通知其他CPU告知該變數的緩衝行是無效的,因此其他CPU在讀取該變數時,發現其無效會重新從主存中載入資料。

Java記憶體模型

上面從作業系統層次闡述了如何保證資料一致性,下面我們來看一下Java記憶體模型,稍微研究一下Java記憶體模型為我們提供了哪些保證以及在Java中提供了哪些方法和機制來讓我們在進行多線程編程時能夠保證程式執行的正確性。

在並發編程中我們一般都會遇到這三個基本概念:原子性、可見度、有序性。我們稍微看下volatile

原子性

原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

原子性就像資料庫裡面的事務一樣,他們是一個團隊,同生共死。其實理解原子性非常簡單,我們看下面一個簡單的例子即可:

i = 0;            ---1j = i ;            ---2i++;            ---3i = j + 1;    ---4

上面四個操作,有哪個幾個是原子操作,那幾個不是?如果不是很理解,可能會認為都是原子性操作,其實只有1才是原子操作,其餘均不是。

1—在Java中,對基礎資料型別 (Elementary Data Type)的變數和賦值操作都是原子性操作;
2—包含了兩個操作:讀取i,將i值賦值給j
3—包含了三個操作:讀取i值、i + 1 、將+1結果賦值給i;
4—同三一樣

在單線程環境下我們可以認為整個步驟都是原子性操作,但是在多線程環境下則不同,Java只保證了基礎資料型別 (Elementary Data Type)的變數和賦值操作才是原子性的(註:在32位的JDK環境下,對64位元據的讀取不是原子性操作*,如long、double)。要想在多線程環境下保證原子性,則可以通過鎖、synchronized來確保。

volatile是無法保證複合操作的原子性

可見度

可見度是指當多個線程訪問同一個變數時,一個線程修改了這個變數的值,其他線程能夠立即看得到修改的值。

在上面已經分析了,在多線程環境下,一個線程對共用變數的操作對其他線程是不可見的。

Java提供了volatile來保證可見度。

當一個變數被volatile修飾後,表示著執行緒區域記憶體無效,當一個線程修改共用變數後他會立即被更新到主記憶體中,當其他線程讀取共用變數時,它會直接從主記憶體中讀取。
當然,synchronize和鎖都可以保證可見度。

有序性

有序性:即程式執行的順序按照代碼的先後順序執行。

在Java記憶體模型中,為了效率是允許編譯器和處理器對指令進行重排序,當然重排序它不會影響單線程的運行結果,但是對多線程會有影響。

Java提供volatile來保證一定的有序性。最著名的例子就是單例模式裡面的DCL(雙重檢查鎖)。這裡LZ就不再闡述了。

剖析volatile原理

JMM比較龐大,不是上面一點點就能夠闡述的。上面簡單地介紹都是為了volatile做鋪墊的。

volatile可以保證線程可見度且提供了一定的有序性,但是無法保證原子性。在JVM底層volatile是採用“記憶體屏障”來實現的。

上面那段話,有兩層語義

  1. 保證可見度、不保證原子性
  2. 禁止指令重排序

第一層語義就不做介紹了,下面重點介紹指令重排序。

在執行程式時為了提高效能,編譯器和處理器通常會對指令做重排序:

  1. 編譯器重排序。編譯器在不改變單線程程式語義的前提下,可以重新安排語句的執行順序;
  2. 處理器重排序。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序;

指令重排序對單線程沒有什麼影響,他不會影響程式的運行結果,但是會影響多線程的正確性。既然指令重排序會影響到多線程執行的正確性,那麼我們就需要禁止重排序。那麼JVM是如何禁止重排序的呢?這個問題稍後回答,我們先看另一個原則happens-before,happen-before原則保證了程式的“有序性”,它規定如果兩個操作的執行順序無法從happens-before原則中推到出來,那麼他們就不能保證有序性,可以隨意進行重排序。其定義如下:

  1. 同一個線程中的,前面的操作 happen-before 後續的操作。(即單線程內按代碼順序執行。但是,在不影響在單線程環境執行結果的前提下,編譯器和處理器可以進行重排序,這是合法的。換句話說,這一是規則無法保證編譯重排和指令重排)。
  2. 監視器上的解鎖操作 happen-before 其後續的加鎖操作。(Synchronized 規則)
  3. 對volatile變數的寫操作 happen-before 後續的讀操作。(volatile 規則)
  4. 線程的start() 方法 happen-before 該線程所有的後續操作。(線程啟動規則)
  5. 線程所有的操作 happen-before 其他線程在該線程上調用 join 返回成功後的操作。
  6. 如果 a happen-before b,b happen-before c,則a happen-before c(傳遞性)。

我們著重看第三點volatile規則:對volatile變數的寫操作 happen-before 後續的讀操作。為了實現volatile記憶體語義,JMM會重排序,其規則如下:

對happen-before原則有了稍微的瞭解,我們再來回答這個問題JVM是如何禁止重排序的?

觀察加入volatile關鍵字和沒有加入volatile關鍵字時所產生的彙編代碼發現,加入volatile關鍵字時,會多出一個lock首碼指令。lock首碼指令其實就相當於一個記憶體屏障。記憶體屏障是一組處理指示,用來實現對記憶體操作的順序限制。volatile的底層就是通過記憶體屏障來實現的。是完成上述規則所需要的記憶體屏障:

volatile暫且下分析到這裡,JMM體系較為龐大,不是三言兩語能夠說清楚的,後面會結合JMM再一次對volatile深入分析。

總結

volatile看起來簡單,但是要想理解它還是比較難的,這裡只是對其進行基本的瞭解。volatile相對於synchronized稍微輕量些,在某些場合它可以替代synchronized,但是又不能完全取代synchronized,只有在某些場合才能夠使用volatile。使用它必須滿足如下兩個條件:

  1. 對變數的寫操作不依賴當前值;
  2. 該變數沒有包含在具有其他變數的不變式中。

volatile經常用於兩個兩個情境:狀態標記兩、double check

參考資料
  1. 周志明:《深入理解Java虛擬機器》
  2. 方騰飛:《Java並發編程的藝術》
  3. Java並發編程:volatile關鍵字解析
  4. Java 並發編程:volatile的使用及其原理

【死磕Java並發】-----深入分析volatile的實現原理

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.