併發編程九:java內存模型

轉載:https://blog.csdn.net/qq_34964197/article/details/80937147

注意:內存模型和內存區域不是一個概念。(有興趣自己去學習)
明確:內存模型定義了共享內存系統中多線程程序讀寫操作行爲的規範。

一、CPU cache模型

CPU的處理速度和內存的訪問速度差距太大,於是在CPU和主存之間增加了緩存。

CPU cache模型如圖:
在這裏插入圖片描述

  • Cache的出現解決直接訪問內存效率低下。程序運行時,Cache會將運算所需要的數據從主存複製一份到CPU cache中,這樣CPU進行計算時就可以直接對CPU Cache中的數據進行讀取和寫入,運算結束後,再將CPU cache中的最新數據刷入主存中。

  • 按照數據讀取順序和與CPU結合的緊密程度,CPU緩存可以分爲一級緩存(L1),二級緩存(L2),部分高端CPU還具有三級緩存(L3),每一級緩存中所儲存的全部數據都是下一級緩存的一部分。
    這三種緩存的技術難度和製造成本是相對遞減的,所以其容量也是相對遞增的。
    那麼,在有了多級緩存之後,程序的執行就變成了:
    當CPU要讀取一個數據時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內存中查找。

CPU和主存之間交互的架構如圖:
在這裏插入圖片描述

二、單線程、多線程在單核CPU、多核CPU中的影響

單核CPU,多線程。進程中的多個線程會同時訪問進程中的共享數據,CPU將某塊內存加載到緩存後,不同線程在訪問相同的物理地址的時候,都會映射到相同的緩存位置,這樣即使發生線程的切換,緩存仍然不會失效。但由於任何時刻只能有一個線程在執行,因此不會出現緩存訪問衝突。

多核CPU,多線程。每個核都至少有一個L1 緩存。多個線程訪問進程中的某個共享內存,且這多個線程分別在不同的核心上執行,則每個核心都會在各自的caehe中保留一份共享內存的緩衝。由於多核是可以並行的,可能會出現多個線程同時寫各自的緩存的情況,而各自的cache之間的數據就有可能不同。

三、問題

  • 緩存一致性問題(硬件導致)
    在CPU和主存之間增加緩存,在多線程場景下就可能存在緩存一致性問題,也就是說,在多核CPU中,每個核的自己的緩存中,關於同一個數據的緩存內容可能不一致。
  • 處理器優化(硬件導致)
    爲了使處理器內部的運算單元能夠儘量的被充分利用,處理器可能會對輸入代碼進行亂序執行處理。這就是處理器優化。
  • 指令重排(JVM導致)
    很多編程語言的編譯器也會有類似處理器優化的操作,比如Java虛擬機的即時編譯器(JIT)

在併發編程中,有關原子性問題、可見性問題、有序性問題是人們抽象定義出來的。而這個抽象的底層問題就是前面提到的緩存一致性問題、處理器優化問題和指令重排問題等。

  • 原子性是指在一個操作中就是cpu不可以在中途暫停然後再調度,既不被中斷操作,要不執行完成,要不就不執行。
  • 可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
  • 有序性即程序執行的順序按照代碼的先後順序執行。

四、內存模型

前面提到的,緩存一致性問題、處理器器優化的指令重排問題是硬件的不斷升級導致的。那麼,有沒有什麼機制可以很好的解決上面的這些問題呢?
最簡單直接的做法就是廢除處理器和處理器的優化技術、廢除CPU緩存,讓CPU直接和主存交互。但是,這麼做雖然可以保證多線程下的併發問題。但是,這就有點因噎廢食了。
所以,爲了保證併發編程中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是——內存模型。
爲了保證共享內存的正確性(可見性、有序性、原子性),內存模型定義了共享內存系統中多線程程序讀寫操作行爲的規範。通過這些規則來規範對內存的讀寫操作,從而保證指令執行的正確性。它與處理器有關、與緩存有關、與併發有關、與編譯器也有關。他解決了CPU多級緩存、處理器優化、指令重排等導致的內存訪問問題,保證了併發場景下的一致性、原子性和有序性。
內存模型解決併發問題主要採用兩種方式:限制處理器優化和使用內存屏障(具體有興趣自己去學習)。

五、java內存模型

Java程序是需要運行在Java虛擬機上面的,Java內存模型(Java Memory Model ,JMM)就是一種符合內存模型規範的,屏蔽了各種硬件和操作系統的訪問差異的,保證了Java程序在各種平臺下對內存的訪問都能保證效果一致的機制及規範。

提到Java內存模型,一般指的是JDK 5 開始使用的新的內存模型,主要由JSR-133: JavaTM Memory Model and Thread Specification 描述。感興趣的可以參看下這份PDF文檔(http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf)

Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。

而JMM就作用於工作內存和主存之間數據同步過程。他規定了如何做數據同步以及什麼時候做數據同步。

在這裏插入圖片描述
這裏面提到的主內存和工作內存,讀者可以簡單的類比成計算機內存模型中的主存和緩存的概念。特別需要注意的是,主內存和工作內存與JVM內存區域中的Java堆、棧、方法區等並不是同一個層次的內存劃分,無法直接類比。《深入理解Java虛擬機》中認爲,如果一定要勉強對應起來的話,從變量、主內存、工作內存的定義來看,主內存主要對應於Java堆中的對象實例數據部分。工作內存則對應於虛擬機棧中的部分區域。

所以,再來總結下,JMM是一種規範,目的是解決由於多線程通過共享內存進行通信時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題。

六、Java內存模型的實現

瞭解Java多線程的朋友都知道,在Java中提供了一系列和併發處理相關的關鍵字,比如volatile、synchronized、final、concurrent包等。其實這些就是Java內存模型封裝了底層的實現後提供給程序員使用的一些關鍵字。
在開發多線程的代碼的時候,我們可以直接使用synchronized等關鍵字來控制併發,從來就不需要關心底層的編譯器優化、緩存一致性等問題。所以,Java內存模型,除了定義了一套規範,還提供了一系列原語,封裝了底層實現後,供開發者直接使用。每個關鍵字的具體後續的文章都有介紹,請自行查閱。

本文還有一個重點要介紹的就是,我們前面提到,併發編程要解決原子性、有序性和一致性的問題,我們就再來看下,在Java中,分別使用什麼方式來保證。

  • 原子性
    在Java中,爲了保證原子性,提供了兩個高級的字節碼指令monitorenter和monitorexit。在synchronized的實現原理文章中,介紹過,這兩個字節碼,在Java中對應的關鍵字就是synchronized。
    因此,在Java中可以使用synchronized來保證方法和代碼塊內的操作是原子性的。

  • 可見性
    Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值的這種依賴主內存作爲傳遞媒介的方式來實現的。
    Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改後可以立即同步到主內存,被其修飾的變量在每次是用之前都從主內存刷新。因此,可以使用volatile來保證多線程操作時變量的可見性。
    除了volatile,Java中的synchronized和final兩個關鍵字也可以實現可見性。只不過實現方式不同,這裏不再展開了。

  • 有序性
    在Java中,可以使用synchronized和volatile來保證多線程之間操作的有序性。實現方式有所區別:
    volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只允許一條線程操作。

好了,這裏簡單的介紹完了Java併發編程中解決原子性、可見性以及有序性可以使用的關鍵字。讀者可能發現了,好像synchronized關鍵字是萬能的,他可以同時滿足以上三種特性,這其實也是很多人濫用synchronized的原因。

但是synchronized是比較影響性能的,雖然編譯器提供了很多鎖優化技術,但是也不建議過度使用。

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