深入理解Java虛擬機(五)Java內存模型

注意:Java內存模型和Java運行時數據區域是屬於不同層次的概念,請不要混淆。
  Java虛擬機中定義了一種內存模型(即爲Java Memory Model,簡稱JMM)。Java內存模型用來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。在此之前,C/C++直接使用物理硬件和操作系統的內存模型,因此,會由於不同平臺下的的內存模型的差異,有可能導致程序在一套平臺上併發完全正常,而在另一套平臺上併發訪問經常出錯。

1. 主內存與工作內存

  Java內存模型本身是一種抽象的概念,並不真實存在,它描述的是一種規則或規範,通過這組規範定義了程序中各個變量(包括實例域、靜態域和構成數組對象的元素)的訪問方),即在JVM中將變量存儲到內存和從內存中取出變量這樣的細節。此處變量包括實例字段、靜態字段和構成數組對象的元素, 但不包括局部變量和方法參數,因爲後兩者時線程私有的,不會被共享。
  因爲JVM運行程序的實體是線程,而每個線程創建時JVM都會爲其創建一個工作內存,用於存儲線程私有的數據,而Java內存模型中規定所有變量都存儲在主內存主內存時共享內存區域,所有線程都可以訪問但線程對變量的操作(讀取賦值等)必須在工作內存中進行,而不能直接讀寫主內存中的變量。 那麼就要先進行下列的步驟:

  • 將變量從主內存拷貝到線程的工作內存空間;
  • 執行程序,對變量進行操作(讀寫賦值等);
  • 操作完成後,將變量寫回主內存。

  工作內存中存儲着主內存的變量副本拷貝,工作內存是線程私有的數據區域,所以不同線程之間無法訪問對象的工作內存,線程通信(變量值的傳遞)必須通過主內存來完成。
線程、主內存、工作內存三者之間的交互關係如下圖:
在這裏插入圖片描述
  上面我們說過JMM與Java內存區域不是同一層次的概念,但是我們可能在初學習中經常會搞混,它們肯定會有一定的相似性,下來我們就來說說它們的相似點。JMM與Java內存區域唯一相似點,都存在共享數據區域和私有數據區域,在JMM中主內存屬於共享數據區域,從某個程度上講應該包括了堆和方法區,而工作內存數據線程私有數據區域,從某個程度上講則應該包括程序計數器、虛擬機棧以及本地方法棧。或許在某些地方,我們可能會看見主內存被描述爲堆內存,工作內存被稱爲線程棧,實際上他們表達的都是同一個含義。

  • 主內存
    主要存儲的是Java實例對象,所有線程創建的實例對象都存放在主內存中,不管該實例對象是成員變量還是方法中的本地變量(也稱局部變量),除此之外還包含共享的類信息、常量、靜態變量。由於是共享數據區域,多條線程對同一變量進行訪問可能會發生線程安全問題。
  • 工作內存
    主要存儲的是當前方法的所有本地變量信息(工作內存中存儲着主內存中的變量副本拷貝),每個線程只能訪問自己的工作內存,即線程中的本地變量對其他線程是不可見的,就算兩個線程執行的是同一段代碼,它們也會各自在自己的工作內存中創建屬於當前線程的本地變量,由於是線程私有數據區域,也應該包括程序計數器(字節碼行號指示器)、虛擬機棧 以及 本地方法棧(Native方法)。
    注意: 由於工作內存是每個線程的私有數據區域,線程間無法相互訪問工作內存,因此存儲在工作內存中的數據不存在線程安全問題。

2. 內存間交互操作

  關於主內存與工作內存之間的具體交互協議,即一個變量如何從主內存中拷貝到工作內存如何從工作內存同步回主內存之類的實現細節,Java內存模型中定義了下面8種操作來完成。JVM實現是必須保證下面提及的每一種操作的原子性、不可再分性。

  • lock(鎖定):作用於主內存的變量,它把變量標識爲一條線程獨佔的狀態。
  • unlock(解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
  • read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存,以便隨後的load動作使用。
  • load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
  • use(使用):作用於工作內存的變量,它把工作內存中的一個變量的值傳遞給執行引擎。、
  • assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量。
  • store(存儲):作用於工作內存的變量,它把工作內存中的一個變量的值傳送到主內存中,以便後續write操作使用。
  • write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的變量的值放到主內存的變量中。

3. Java內存模型的三大特性

  由於JVM運行程序的實體是線程,而每個線程創建時JVM都會爲其創建一個工作內存(有些地方稱爲棧空間),用於存儲線程私有的數據,線程與主內存中的變量操作必須通過工作內存間接完成,主要過程是將變量從主內存拷貝的每個線程各自的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回主內存,如果存在兩個線程同時對一個主內存中的實例對象的變量進行操作就有可能誘發線程安全問題。
舉例:主內存中存在一個共享變量x,現在有A和B兩條線程分別對該變量x進行操作,A/B線程各自的工作內存中存在共享變量的副本x。
 假設A線程現在要修改x的值,B線程卻想要讀取x的值,那麼B線程讀取到的值時A線程更新後的值2還是更新前的值1呢?
 答案是:不確定,即B線程有可能讀到A線程更新前的值,也有可能讀取到A線程更新後的值。這是因爲工作內存是每個線程私有的數據區域,而線程A修改變量x的時候,首先將變量從主內存拷貝到A線程的工作內存中,然後對變量進行操作,操作完成後再將變量x寫回主內存中;而對於B線程的操作也是類似的,這要就有可能造成主內存與工作內存間存在一致性問題。
 假如A線程修改完後正在將數據寫回主內存,而B線程此時正在讀取主內存,即將x拷貝到自己的工作內存中,這樣B線程讀取到的x值就是A更新前的值,但如果A線程已將更新後的x寫回主內存後,B線程纔開始讀取的話,那麼此時B線程讀取到的x就是A更新後的值,但到底是哪種情況先發生呢?這是不確定的,這也就是所謂的線程安全問題。
在這裏插入圖片描述爲了解決類似上面的線程安全問題,JVM定義了一組規則,通過這組規則來決定一個線程對共享變量的寫入何時對另一個線程課件,這組規則也稱爲Java內存模型(即JMM),JMM圍繞着程序執行的原子性、有序性、可見性展開的。

  • 原子性: 指一個操作是不可中斷的,要麼不執行,要麼執行且在執行過程中不會被任何因素打斷。由Java內存模型來直接保證的原子性變量操作包括:read、load、assign、use、store和read。大致可以認爲,基本數據類型的訪問讀寫是具備原子性的(64位虛擬機)。如果需要大範圍的原子性,就需要synchronized關鍵字約束。
  • 可見性: 指當一個線程修改了共享變量的值,其他線程就能立即知道這個修改操作。volatile、synchronized、final三個關鍵字可以實現可見性。
  • 有序性: 指對於單線程的執行的代碼,總是認爲代碼的執行是按順序依次執行的,但在線程中觀察另外一個線程,所有的操作都是無序的。前半句是指“線程內表現爲串行”,後半句是指“指令重排序”和“工作內存與主內存同步延遲現象”。

4. JMM中的happens-before原則(先行發生原則)

Java內存模型具備一些先天的“有序性”,既不需要通過任何手段就能夠保證的有序性,這個操作通常也稱爲happens-before原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那麼就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
 happens-before原則的內容如下所示:

  • 程序次序規則: 一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。
  • 鎖定規則: 一個unlock操作先行發生於對同一個鎖的lock操作。《加鎖在解鎖前》
  • volatile變量規則: 對一個變量的寫操作,先行發生於後面對這個變量的讀操作。
  • 傳遞規則: 如果操作A先行發生於操作B,而操作B又先行發生於C,則可以得出操作A先行發生於操作C。《A在B前,B在C前 => A在C前》
  • 線程啓動規則: Thread對象的start()方法先行發生於此線程的每一個動作。
  • 線程中斷規則: 對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
  • 線程終結規則: 線程中所有的操作都先行發生於線程終止檢測,我們可以通過Thread.join()方法結束Thread.isAlive()的返回值手段檢測到線程已經終止執行。
  • 對象終結規則: 一個對象的初始化完成先行發生於它的finalize()方法的開始。

也就是說,要想併發程序正確的執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行的不正確。

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