Java併發編程-volatile

来源:https://www.cnblogs.com/iou123lg/archive/2018/07/08/9280639.html
-Advertisement-
Play Games

上一篇文章,學習了併發編程中的synchronized,這個比較好理解,也是我最初學習多線程編程中的一個簡單的實現的,大學的時候就會了,然後就一直以為多線程環境的同步只能通過這個來實現的,事實上Java還提供了另外一個更加輕量級的實現-volatile,如果說synchronized實現了數據在同一 ...


  上一篇文章,學習了併發編程中的synchronized,這個比較好理解,也是我最初學習多線程編程中的一個簡單的實現的,大學的時候就會了,然後就一直以為多線程環境的同步只能通過這個來實現的,事實上Java還提供了另外一個更加輕量級的實現-volatile,如果說synchronized實現了數據在同一時刻只能有一個線程對數據訪問的話,那麼volatile實現的就是同時可以多個線程在訪問數據,但是只要數據發生了變化,便確保其他線程及時“感知”這種變化。

       1、CPU、主存及高速緩存的概念

  電腦的硬體組成可以抽象為由匯流排、IO設備、主存、處理器(CPU)等組成。其中數據存放在主存中,CPU負責指令的執行,CPU的指令執行非常快,大部分簡單指令的執行只需要一個時鐘周期,而一次主記憶體數據的讀取則需要幾十到幾百個時鐘周期,那麼CPU從主存中讀寫數據就會有很大的延遲。這個時候就產生了高速緩存的概念。

  也就是說,當程式在運行過程中,會將運算需要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據回寫到主存當中,通過這種方式來降低CPU從主存中獲取數據的延遲。大致的示意圖如下:

  圖一這個模型,可以簡單的認為是單核模型,在這個模型裡面,以i++這個操作為例,程式執行時,會先從主記憶體中獲取i的值,複製到高速緩存,然後CPU從高速緩存中載入並執行+1操作,操作完成後回寫到高速緩存,最後再從高速緩存回寫到主記憶體。單核模型這樣操作沒有任何問題,但是電腦自產生以來,一直追求的兩個目標,一個是如何做的更多,另一個就是如何計算得更快,這樣帶來的變化就是單核變成多核,高速緩存分級存儲。大致的示意圖如下:

  在圖二示意圖裡面,i++這個操作就有問題了,因為多核CPU可以線程並行計算,在Core 0和Core 1中可以同時將i複製到各自緩存中,然後CPU各自進行計算,假設初始i為1,那麼預期我們希望是2,但是實際由於兩個CPU各自先後計算後最終主記憶體中的i可能是2,也可能是其他值。

  這個就是硬體記憶體架構中存在的一個問題,緩存一致性問題,就是說核1改變了變數i的值之後,核0是不知道的,存放的還是舊值,最終對這樣的一個臟數據進行操作。

  為此,CPU的廠商定製了相關的規則來解決這樣一個硬體問題,主要有如下方式:

  1)  匯流排加鎖,其實很好理解匯流排鎖,咱們來看圖二,前面提到了變數會從主記憶體複製到高速緩存,計算完成後,會再回寫到主記憶體,而高速緩存和主記憶體的交互是會經過匯流排的。既然變數在同一時刻不能被多個CPU同時操作,會帶來臟數據,那麼只要在匯流排上阻塞其他CPU,確保同一時刻只能有一個CPU對變數進行操作,後續的CPU讀寫操作就不會有臟數據。匯流排鎖的缺點也很明顯,有點類似將多核操作變成單核操作,所以效率低;

  2)  緩存鎖,即緩存一致性協議,主要有MSI、MESI、MOSI等,這些協議的主要核心思想:當CPU寫數據時,如果發現操作的變數是共用變數,即在其他CPU中也存在該變數的副本,會發出信號通知其他CPU將該變數的緩存行置為無效狀態,因此當其他CPU需要讀取這個變數時,發現自己緩存中緩存該變數的緩存行是無效的,那麼它就會從記憶體重新讀取。

  2、Java記憶體模型

  在Java虛擬機規範中試圖定義一種Java記憶體模型(Java Memory Model,JMM)來屏蔽各個硬體平臺和操作系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果。在此之前,主流程式語言(C/C++等)直接使用物理硬體和操作系統的記憶體模型(可以理解為類似於直接使用了硬體標準),都或多或少的在不同的平臺有著不一樣的執行結果。 

  Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即變數在記憶體中的存儲和從記憶體中取出變數這樣的底層細節。其規定了所有變數都存儲在主記憶體,每個線程還有自己的工作記憶體,線程讀寫變數時需先複製到工作記憶體,執行完計算操作後再回寫到主記憶體,每個線程還不能訪問其他線程的工作記憶體。大致示意圖如下:

  圖三我們可以理解為和圖二表達的是一個意思,工作記憶體可以看成是CPU高速緩存、寄存器的抽象,主記憶體可以看成就是物理硬體中主記憶體的抽象,圖二這個模型會存在緩存一致性問題,圖三同樣也會存在緩存一致性問題。

  另外,為了獲得較好的執行性能,Java記憶體模型並沒有限制執行引擎使用處理器的寄存器或者高速緩存來提升指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在Java記憶體模型中,還會存在指令重排序的問題。

  Java語言又是怎麼來解決這兩個問題的呢?就是通過volatile這個關鍵字來解決緩存一致性和指令重排問題,volatile作用就是確保可見性和禁止指令重排。

3、volatile背後實現

  那麼volatile又是怎樣來確保的可見性和禁止指令重排呢?咱們先來寫一段單例模式代碼來看看。

public class Singleton {
    private static volatile  Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

先看看位元組碼層面,JVM都做了什麼。

                        圖四

從圖四可以看出,沒有什麼特別之處。既然在位元組碼層面我們看不出什麼端倪,那下麵就看看將代碼轉換為彙編指令能看出什麼端倪。轉換為彙編指令,可以通過-XX:+PrintAssembly來實現,window環境具體如何操作請參考此處(https://dropzone.nfshost.com/hsdis.xht)。不過比較可惜的是我雖然編譯成功了hsdis-i386.dll(圖五),放置在了JDK8下的多個bin目錄,一致在報找不到這個dll文件所以我決定換個思路一窺究竟。

                  圖五

這個思路就是去閱讀openJDK的源代碼。其實通過javap可以看到volatile位元組碼層面有個關鍵字ACC_VOLATILE,通過這個關鍵字定位到accessFlags.hpp文件,代碼如下:

bool is_volatile    () const         { return (_flags & JVM_ACC_VOLATILE    ) != 0; }

再搜索關鍵字is_volatile,在bytecodeInterpreter.cpp可以看到如下代碼:

//
          // Now store the result
          //
          int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) {
            if (tos_type == itos) {
              obj->release_int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {
              obj->release_byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {
              obj->release_long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {
              obj->release_char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {
              obj->release_short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {
              obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
            } else {
              obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
            }
            OrderAccess::storeload();
          }

在這段代碼中,會先判斷tos_type,後面分別有不同的基礎類型的實現,比如int就調用release_int_field_put,byte就調用release_byte_field_put等等。以int類型為例,繼續搜索方法release_int_field_put,在oop.hpp可以看到如下代碼:

void release_int_field_put(int offset, jint contents);

這段代碼實際是內聯oop.inline.hpp,具體的實現是這樣的:

inline void oopDesc::release_int_field_put(int offset, jint contents)       { OrderAccess::release_store(int_field_addr(offset), contents);  }

其實看到這,可以看到上一篇文章很熟悉的oop.hpp和oop.inline.hpp,就是很熟悉的Java對象模型。繼續看OrderAccess::release_store,可以在orderAccess.hpp找到對應的實現方法:

static void     release_store(volatile jint*    p, jint    v);

實際上這個方法的實現又有很多內聯的針對不同的CPU有不同的實現的,在src/os_cpu目錄下可以看到不同的實現,以orderAccess_linux_x86.inline.hpp為例,是這麼實現的:

inline void     OrderAccess::release_store(volatile jint*    p, jint    v) { *p = v; }

可以看到其實Java的volatile操作,在JVM實現層面第一步是給予了C++的原語實現,接下來呢再看bytecodeInterpreter.cpp截取的代碼,會再給予一個OrderAccess::storeload()操作,而這個操作執行的代碼是這樣的(orderAccess_linux_x86.inline.hpp):

inline void OrderAccess::storeload()  { fence(); }

fence方法代碼如下:

inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}

一樣可以看到和通過-XX:+PrintAssembly來看到的背後實現:lock; addl,其實這個就是記憶體屏障,關於記憶體屏障的詳細說明可以看下orderAccess.hpp的註釋。記憶體屏障提供了3個功能:確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;強制將對緩存的修改操作立即寫入主存;如果是寫操作,它會導致其他CPU中對應的緩存行無效。這3個功能又是怎麼做到的呢?來看下記憶體屏障的策略:

在每個volatile寫操作前面插入storestore屏障;

在每個volatile寫操作後面插入storeload屏障;

在每個volatile讀操作後面插入loadload屏障;

在每個volatile讀操作後面插入loadstore屏障;

其中loadload和loadstore對應的是方法acquire,storestore對應的是方法release,storeload對應的是方法fence。

4、volatile應用場景

 4.1 double check單例

public class Singleton {
    private static volatile  Singleton instance;
    private Singleton() {};
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

為什麼要這樣寫,這個網上有很多資料,這裡就不贅述了。

4.2 java.util.concurrent

大量的應用在j.u.c下的各個基礎類和工具欄,構成Java併發包的基礎。後續併發編程的學習就可以按照這個路線圖來學習了。

參考資料:

https://github.com/lingjiango/ConcurrentProgramPractice

https://stackoverflow.com/questions/4885570/what-does-volatile-mean-in-java

https://stackoverflow.com/questions/106591/do-you-ever-use-the-volatile-keyword-in-java

https://www.cnblogs.com/zhangj95/p/5647051.html

http://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec

https://www.cs.umd.edu/~pugh/java/memoryModel/


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 微服務概念已經非常流行,這影響了現在架構的思想潮流。 如今,使用spring cloud體系搭建微服務架構的公司越來越多,成本低,出線上產品快,模塊全,開源等原因未來可能更加流行。 一般,我們需要一個監控系統來監控應用的數據,比如記憶體,磁碟,線程情況,資料庫連接池,配置信息,jvm信息等等。 ...
  • 大家好,上篇文章為大家介紹了線程間通信和協作的一些基本方式,那這篇文章就來介紹一下經典的wait-notify機制吧。 什麼是wait-notify機制? 想象一下有兩個線程A、B,如果業務場景中需要這兩個線程交替執行任務(比如A執行完一次任務後換B執行,B執行完後再換A執行這樣重覆交替),之前的基 ...
  • 轉自:https://www.cnblogs.com/Lynn-Zhang/p/5377024.html C/C++程式編譯流程(預處理->編譯->彙編->鏈接) 程式的基本流程如圖: 1. 預處理 預處理相當於根據預處理指令組裝新的C/C++程式。經過預處理,會產生一個沒有巨集定義,沒有條件編譯指令 ...
  • 一、概念 先來整體的介紹一下這篇博文要介紹的幾個概念(Channel、ChannelHandler、ChannelPipeline、ChannelHandlerContext、ChannelPromise): Channel:Netty 中傳入或傳出數據的載體;ChannelHandler:Nett ...
  • Description 對於任何正整數x,其約數的個數記作g(x)。例如g(1)=1、g(6)=4。如果某個正整數x滿足:g(x)>g(i) 0<i<x ,則稱x為反質數。例如,整數1,2,4,6等都是反質數。現在給定一個數N,你能求出不超過N的最大的反質數麽 ? 對於任何正整數x,其約數的個數記作 ...
  • 引言 古人雲:“活到老,學到老。”互聯網算是最辛苦的行業之一,“加班”對工程師來說已是“家常便飯”,同時互聯網技術又日新月異,很多工程師都疲於應付,叫苦不堪。以至於長期以來流傳一個很廣的誤解:35歲是程式員工作的終點。 如何在繁忙的工作中做好技術積累,構建個人核心競爭力,相信是很多工程師同行都在思考 ...
  • 什麼是 Spring MVC 學習某一樣東西之前,我們一定要大致知道這個東西是什麼,能幹什麼,為什麼要用它。 Spring MVC 是一個開源平臺,一個基於 Spring 的 MVC 框架,它支持基於 Java 開發 Web 應用程式。MVC 架構很利於開發靈活、低耦合的 Web 應用程式組件。 M ...
  • 題目背景 無 題目描述 在一個凹槽中放置了 n 層磚塊、最上面的一層有n 塊磚,從上到下每層依次減少一塊磚。每塊磚 都有一個分值,敲掉這塊磚就能得到相應的分值,如下圖所示。 如果你想敲掉第 i 層的第j 塊磚的話,若i=1,你可以直接敲掉它;若i>1,則你必須先敲掉第 i-1 層的第j 和第j+1 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...