Java內存模型和物理架構詳解

1. 概述

  我們常說的Java虛擬機具有很好的跨平臺性,之所以強調所謂的跨平臺性是因爲不同的系統底層架構是會有區別的,而Java虛擬機的跨平臺性就是它幫助我們把不同系統的底層區別給KO掉了,使得我們通過Java虛擬機編寫的代碼可以忽略不同平臺底層的區別。而這其中關鍵的一點就是Java虛擬機的Java內存模型,內存模型其實就是JVM內部自己設計了一套規則,這套規則會規定不同線程之間信息同步的規則、訪問規則以及修改規則等等,在這些規則的條件下,對於底層不同系統的底層差異就不需要開發人員關心了。
  雖然不同的系統的硬件架構會有區別,但是整體來說是相似的,所以我們在理解Java內存模型設計前,通過了解內存硬件架構會對Java內存模型的理解有很好的幫助。

2. 硬件內存架構

  對於硬件內存架構的理解首先涉及到三個重要的概念:CPU寄存器、CPU緩存以及主存,CPU在訪問這三者的速度而言差別是很大的,關係是:CPU寄存器>CPU緩存>主存,最開始的時候是沒有CPU緩存一說的,之所以出現它就是因爲CPU寄存器和主存之間訪問速度差別太大,導致計算機性能有較大缺陷,所以後來在中間加入了CPU緩存來適當解決這個問題,這一塊三者之間的關係和區別不再贅述,可以參考:https://blog.csdn.net/hellojoy/article/details/54744231
瞭解了以上三個概念後,我們來看看硬件內存架構圖:
硬件內存架構圖
  現在的計算機多數都是多個CPU的,一個CPU可能是多核,如圖所示每個CPU內部有自己的一個CPU寄存器,多個CPU共享主存,然後由於CPU訪問主存速度相比CPU寄存器要慢很多,所以每個CPU和主存之間又有一箇中間的過度區域爲CPU緩存。
  以我們所謂的多線程應用來說,要知道的一點是線程和進程的區別,簡單理解就是進程就是一個應用,系統在進行資源分配的時候是給進程進行分配的,而線程則是依託於進程的更小的單位,一個進程內部可以有多個線程,多線程之間會共享系統分配給該進程的主存空間,線程是CPU進行調度的基本單位,CPU在分配時間片時是分配到線程級別的。所以上圖的硬件內存架構中,可以清楚的看到多個CPU之間是共享主存的,這就好比我們說的多個線程之間是共享系統分配給進程的主存空間類似。
  以上就是關於硬件內存架構的介紹,那麼接下來我們再來看Java內存模型的介紹。

3. Java內存模型

  首先,看到這裏的人都應該是知道JVM內存劃分主要可以簡單的分爲線程棧和堆(其它的方法區、程序計數器等忽略),對於堆和棧的內存使用來說,其實模式是和剛纔介紹的硬件內存架構是基本一致的,如下:
JVM內存劃分
  可以看到圖中意思就是,一個JVM內部的內存佈局,每個線程都會有自己的一個線程棧,然後我們經常在多線程的時候會需要進行線程間通信,那麼多個線程棧之間建立聯繫的方式就是通過所有線程棧所共享的堆內存。
  這裏我們先來簡單介紹一下線程棧和堆中所存儲的內容,對於堆來說,應用中所有產生的對象都會分配在堆中(不包括基本類型的局部變量),然後對於線程棧來說,它的結構就是一個棧,出棧入棧的就是一個個的棧幀,一個棧幀對應着一個方法的調用,我們在Java編碼的時候,其實各種邏輯啥的都是在方法中實現的,包括主程序入口也是在main方法中開始的,所以一個Java應用整個生命週期就是不斷的各種方法的調用,然後對於每個方法的調用都會封裝爲一個個的棧幀,funcA -> funcB -> funcD這種調用鏈來說就會在對應這個線程棧中插入三個棧幀,每個棧幀中會記錄方法調用的一些局部變量信息、上個調用方法的返回地址等信息,在funcD這個棧幀中處理邏輯完畢後,會把返回值傳遞給記錄的返回地址處,然後回到funcB對應的棧幀中,如此過程就形成的程序的運行過程。每個棧幀的大小在程序編譯的時候就已經是確定了的,而且棧的最大所需深度也是確定的。
  另外對於各種局部變量而言,某些局部變量可能不是簡單的基本類型,而是某個類,那麼這個時候在棧幀中存儲的只是對於這個對應的引用,而對象的具體內容是存儲在堆中的,在需要使用到對象的時候通過引用去堆中獲取對象的信息,我們先來看一個圖:
堆棧信息
  上圖表示有兩個線程棧,其實它內部的代碼是完全一樣的,例如可能是多個線程執行某個一樣的邏輯,然後這兩個線程中可能就會涉及到共享某個變量的情況,這個時候就會出現我們常說的所謂線程安全問題了!具體解釋這一點,我們首先要知道如果多個線程共享某個變量,那麼它們是如何訪問這個存在於堆中的共享變量的呢?線程在需要獲取堆中信息的時候,不是我們所謂的簡單拿過來用完再還回去,而是會把這個堆中的數據拷貝一份作爲自己這個線程的私有變量形式來使用,這個時候相當於堆中會有一份原始數據,然後線程工作的私有內存中也會有一份拷貝過來的數據,然後在使用完畢後,如果數據有變化在把變化的數據更新到堆中。此時如果沒有鎖的話,就會出現兩個線程同一時間都把堆中某個共享變量拷貝到自傢俬有內存中進行處理完畢,然後假設都做了變化,那麼再把變化更新到堆中的時候其中一個線程的數據會被另外一個線程的數據所覆蓋掉,這樣就會出現所謂的線程不安全。所以爲了解決上面的問題,Java內存模型就會規範關於變量操作的一些規則,在這個規則之下可以避免這種情況的出現。
  這裏還有一個問題就是,Java中的堆棧是對應分配在硬件內存架構中的哪塊區域呢?對於第二個問題,我們在開頭說的CPU寄存器、CPU緩存以及主存介紹的帖子中可以看到一點是,CPU寄存器的大小一般在512KB左右,CPU緩存大小一般在12MB這種量級,然後我們在考慮我們平常Java虛擬機堆棧內存分配都很可能分配在GB級別,所以很顯然堆棧應該是主要分配在主存中的,當然網上有說有部分堆棧信息可能會出現在CPU緩存或是寄存器中,這個個人不是很瞭解,有人知道這部分內容的可以介紹一下。
  知道以上內容後,我們再來簡單介紹一下volatilesynchronize關鍵字在這裏面的實現原理,對於volatile而言,如果是多個線程共享的變量,在被volatile修飾後,那麼對於這個變量的所有讀取和修改操作都會直接從主存中進行,而不是和之前說的那樣把這個對象拷貝一份到線程本地作爲私有變量操作。這樣就可以使得如果線程A對共享變量進行了修改,然後線程B可以立刻看到主存中的最新值,不過這種只是保證瞭如果值更新後可以馬上能讓其它線程看到,但是這也不是線程安全的,因爲可能線程A從主存讀取變量值還沒來得及修改更新回主存,然後線程B就來獲取了主存中的舊值然後也進行修改,這樣最終還是會出現某一個線程的操作被覆蓋。所以volatile只能保證可見性但是還是線程不安全,爲了解決這個問題就引入了synchronized關鍵字,這個關鍵字的作用就是保住同一個時刻只能有一個線程訪問到synchronized所修飾的變量或是代碼區域,並且變量的所有讀取更新操作都是直接操作主存的,處理完畢後所有更新同步到主存完畢後纔會開發這個區域讓線程B來訪問,此時就不會出現線程不安全的問題了。

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