網上有不少關於 Java 內存模型的文章,可是不少人讀完以後仍是搞不清楚,甚至有的人說本身更懵了。java
本文就來總體的介紹一下 Java 內存模型,讀完本文之後,你就知道到底 Java 內存模型是什麼,爲何要有 Java 內存模型,Java 內存模型解決了什麼問題等。程序員
本文中不少說法都是筆者本身理解後定義出來的。但願可以讓讀者能夠對 Java 內存模型有更加清晰的認識。spring
爲何要有內存模型編程
在介紹 Java 內存模型以前,咱們先來看一下到底什麼是計算機內存模型,而後再來看 Java 內存模型在計算機內存模型的基礎上都作了哪些事情。緩存
要說計算機的內存模型,就要說一段古老的歷史,看一下爲何要有內存模型。安全
內存模型:英文名 Memory Model,它是一個老古董了。它是與計算機硬件有關的一個概念。那麼,我先介紹下它和硬件到底有啥關係。性能優化
CPU 和緩存一致性微信
咱們應該知道,計算機在執行程序的時候,每條指令都是在 CPU 中執行的,而執行的時候,又免不了和數據打交道。多線程
而計算機上面的數據,是存放在主存當中的,也就是計算機的物理內存。架構
剛開始,還相安無事,可是隨着 CPU 技術的發展,CPU 的執行速度愈來愈快。
而因爲內存的技術並無太大的變化,因此從內存中讀取和寫入數據的過程和 CPU 的執行速度比起來差距就會愈來愈大,這就致使 CPU 每次操做內存都要耗費不少等待時間。
這就像一家創業公司,剛開始,創始人和員工之間工做關係其樂融融,可是隨着創始人的能力和野心愈來愈大,逐漸和員工之間出現了差距,普通員工愈來愈跟不上 CEO 的腳步。
老闆的每個命令,傳達到基層員工以後,因爲基層員工的理解能力、執行能力的欠缺,就會耗費不少時間。這也就無形中拖慢了整家公司的工做效率。
但是,不能由於內存的讀寫速度慢,就不發展 CPU 技術了吧?總不能讓內存成爲計算機處理的瓶頸吧?
因此,人們想出來了一個好的辦法,就是在 CPU 和內存之間增長高速緩存。
緩存的概念你們都知道,就是保存一份數據拷貝。它的特色是速度快,內存小,而且價格昂貴。
那麼,程序的執行過程就變成了:程序在運行過程當中,會將運算須要的數據從主存複製一份到 CPU 的高速緩存當中。
那麼 CPU 進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束以後,再將高速緩存中的數據刷新到主存當中。
以後,這家公司開始設立中層管理人員,管理人員直接歸 CEO 領導,領導有什麼指示,直接告訴管理人員,而後就能夠去作本身的事情了。管理人員負責去協調底層員工的工做。
由於管理人員是瞭解手下的人員以及本身負責的事情的。因此大多數時候,公司的各類決策,通知等,CEO 只要和管理人員之間溝通就夠了。
而隨着 CPU 能力的不斷提高,一層緩存就慢慢的沒法知足要求了,就逐漸的衍生出多級緩存。
按照數據讀取順序和與 CPU 結合的緊密程度,CPU 緩存能夠分爲一級緩存(L1),二級緩存(L2),部分高端 CPU 還具備三級緩存(L3),每一級緩存中所儲存的所有數據都是下一級緩存的一部分。
這三種緩存的技術難度和制形成本是相對遞減的,因此其容量也是相對遞增的。
那麼,在有了多級緩存以後,程序的執行就變成了:當 CPU 要讀取一個數據時,首先從一級緩存中查找,若是沒有找到再從二級緩存中查找,若是仍是沒有就從三級緩存或內存中查找。
隨着公司愈來愈大,老闆要管的事情愈來愈多,公司的管理部門開始改革,開始出現高層,中層,底層等管理者。一級一級之間逐層管理。
單核 CPU 只含有一套 L1,L2,L3 緩存;若是 CPU 含有多個核心,即多核 CPU,則每一個核心都含有一套 L1(甚至和 L2)緩存,而共享 L3(或者和 L2)緩存。
公司也分不少種,有些公司只有一個大 Boss,他一我的說了算。可是有些公司有好比聯席總經理、合夥人等機制。
單核 CPU 就像一家公司只有一個老闆,全部命令都來自於他,那麼就只須要一套管理班底就夠了。
多核 CPU 就像一家公司是由多個合夥人共同創辦的,那麼,就須要給每一個合夥人都設立一套供本身直接領導的高層管理人員,多個合夥人共享使用的是公司的底層員工。
還有的公司,不斷壯大,開始拆分出各個子公司。各個子公司就是多個 CPU 了,互相以前沒有共用的資源。互不影響。
下圖爲一個單 CPU 雙核的緩存結構:
隨着計算機能力不斷提高,開始支持多線程。那麼問題就來了,咱們分別來分析下單線程、多線程在單核 CPU、多核 CPU 中的影響。
單線程:CPU 核心的緩存只被一個線程訪問。緩存獨佔,不會出現訪問衝突等問題。
單核 CPU,多線程:進程中的多個線程會同時訪問進程中的共享數據,CPU 將某塊內存加載到緩存後,不一樣線程在訪問相同的物理地址的時候,都會映射到相同的緩存位置,這樣即便發生線程的切換,緩存仍然不會失效。
但因爲任什麼時候刻只能有一個線程在執行,所以不會出現緩存訪問衝突。
多核 CPU,多線程:每一個核都至少有一個 L1 緩存。多個線程訪問進程中的某個共享內存,且這多個線程分別在不一樣的核心上執行,則每一個核心都會在各自的 Cache 中保留一份共享內存的緩衝。
因爲多核是能夠並行的,可能會出現多個線程同時寫各自的緩存的狀況,而各自的 Cache 之間的數據就有可能不一樣。
在 CPU 和主存之間增長緩存,在多線程場景下就可能存在緩存一致性問題,也就是說,在多核 CPU 中,每一個核的本身的緩存中,關於同一個數據的緩存內容可能不一致。
若是這家公司的命令都是串行下發的話,那麼就沒有任何問題。
若是這家公司的命令都是並行下發的話,而且這些命令都是由同一個 CEO 下發的,這種機制是也沒有什麼問題。由於他的命令執行者只有一套管理體系。
若是這家公司的命令都是並行下發的話,而且這些命令是由多個合夥人下發的,這就有問題了。
由於每一個合夥人只會把命令下達給本身直屬的管理人員,而多個管理人員管理的底層員工多是公用的。
好比,合夥人 1 要辭退員工 a,合夥人 2 要給員工 a 升職,升職後的話他再被辭退須要多個合夥人開會決議。兩個合夥人分別把命令下發給了本身的管理人員。
合夥人 1 命令下達後,管理人員 a 在辭退了員工後,他就知道這個員工被開除了。
而合夥人 2 的管理人員 2 這時候在沒獲得消息以前,還認爲員工 a 是在職的,他就欣然的接收了合夥人給他的升職 a 的命令。
處理器優化和指令重排
上面提到在 CPU 和主存之間增長緩存,在多線程場景下會存在緩存一致性問題。
除了這種狀況,還有一種硬件問題也比較重要。那就是爲了使處理器內部的運算單元可以被充分利用,處理器可能會對輸入代碼進行亂序執行處理。這就是處理器優化。
除了如今不少流行的處理器會對代碼進行優化亂序處理,不少編程語言的編譯器也會有相似的優化,好比 Java 虛擬機的即時編譯器(JIT)也會作指令重排。
可想而知,若是任由處理器優化和編譯器對指令重排的話,就可能致使各類各樣的問題。
關於員工組織調整的狀況,若是容許人事部在接到多個命令後進行隨意拆分亂序執行或者重排的話,那麼對於這個員工以及這家公司的影響是很是大的。
併發編程的問題
前面說的和硬件有關的概念你可能聽得有點蒙,還不知道他到底和軟件有啥關係。
可是關於併發編程的問題你應該有所瞭解了,好比原子性問題,可見性問題和有序性問題。
其實,原子性問題,可見性問題和有序性問題是人們抽象定義出來的。而這個抽象的底層問題就是前面提到的緩存一致性問題、處理器優化問題和指令重排問題等。
這裏簡單回顧下這三個問題,咱們說,併發編程,爲了保證數據的安全,須要知足如下三個特性:
有沒有發現,緩存一致性問題其實就是可見性問題。而處理器優化是能夠致使原子性問題的。指令重排即會致使有序性問題。
因此,後文將再也不提起硬件層面的那些概念,而是直接使用你們熟悉的原子性、可見性和有序性。
什麼是內存模型
前面提到的,緩存一致性問題、處理器優化的指令重排問題是硬件的不斷升級致使的。那麼,有沒有什麼機制能夠很好的解決上面的這些問題呢?
最簡單直接的作法就是廢除處理器和處理器的優化技術、廢除 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、Concurren 包等。
其實這些就是 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 是比較影響性能的,雖然編譯器提供了不少鎖優化技術,可是也不建議過分使用。
總結
在讀完本文以後,相信你應該瞭解了什麼是 Java 內存模型、Java 內存模型的做用以及 Java 中內存模型作了什麼事情等。
注:關注做者微信公衆號,瞭解更多分佈式架構、微服務、netty、MySQL、spring、、性能優化、等知識點。
公衆號:《 Java大蝸牛 》