1. 程式人生 > >Java多執行緒之執行緒安全(0)Java記憶體區域與Java記憶體模型

Java多執行緒之執行緒安全(0)Java記憶體區域與Java記憶體模型

概況

本文內容

1.Java記憶體區域劃分
2.Java記憶體模型JMM
3.硬體記憶體架構與Java記憶體模型
4.Jvm中執行緒實現機制
5.執行緒安全問題的原因

一.理解Java記憶體區域與Java記憶體模型

看下圖

這裡寫圖片描述

1.1Java記憶體區域

各個區域的解釋和功能

方法區(Method Area):

方法區屬於執行緒共享的記憶體區域,又稱Non-Heap(非堆),主要用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,根據Java 虛擬機器規範的規定,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError 異常。值得注意的是在方法區中存在一個叫執行時常量池(Runtime Constant Pool)的區域,它主要用於存放編譯器生成的各種字面量和符號引用

,這些內容將在類載入後存放到執行時常量池中,以便後續使用。

JVM堆(Java Heap):

Java 堆也是屬於執行緒共享的記憶體區域,它在虛擬機器啟動時建立,是Java 虛擬機器所管理的記憶體中最大的一塊,主要用於存放物件例項,幾乎所有的物件例項都在這裡分配記憶體,注意Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做GC 堆,如果在堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError 異常。

程式計數器(Program Counter Register):

屬於執行緒私有的資料區域,是一小塊記憶體空間,主要代表當前執行緒所執行的位元組碼訊號指示器

。位元組碼直譯器工作時,通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

虛擬機器棧(Java Virtual Machine Stacks):

屬於執行緒私有的資料區域,與執行緒同時建立,總數與執行緒關聯,代表Java方法執行的記憶體模型每個方法執行時都會建立一個棧楨來儲存方法的的變量表、運算元棧、動態連結方法、返回值、返回地址等資訊。每個方法從呼叫直結束就對於一個棧楨在虛擬機器棧中的入棧和出棧過程。
如下圖:
這裡寫圖片描述

本地方法棧(Native Method Stacks):

本地方法棧屬於執行緒私有

的資料區域,這部分主要與虛擬機器用到的 Native 方法相關,一般情況下,我們無需關心此區域。

1.2Java記憶體模型

1.2.1概念

Java記憶體模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程式中各個變數(包括例項欄位,靜態欄位和構成陣列物件的元素)的訪問方式。

1.2.2關鍵點

  1. Java記憶體模型中規定所有變數都儲存在主記憶體
  2. JVM排程的實體是執行緒,JVM為各個執行緒建立了私有記憶體空間(工作記憶體);
  3. 執行緒操作要在各自的私有記憶體空間(工作記憶體)中,不能操作共享的主記憶體中的變數,工作記憶體中儲存著主記憶體中的變數副本拷貝;變數從主記憶體拷貝到自己的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫回主記憶體;
  4. JMM是圍繞原子性,有序性、可見性展開的。

1.2.3主記憶體,工作記憶體理解

主記憶體

主要儲存的是Java例項物件,包括成員變數和區域性變數,還有共享的類資訊、常量、靜態變數。由於是共享資料區域,多條執行緒對同一個變數進行訪問可能會發現執行緒安全問題

工作記憶體

主要儲存當前方法的所有區域性變數(工作記憶體中儲存著主記憶體中的變數副本拷貝);每個執行緒只能訪問自己的工作記憶體,即執行緒中的區域性變數對其它執行緒是不可見的,就算是兩個執行緒執行的是同一段程式碼,它們也會各自在自己的工作記憶體中建立屬於當前執行緒的區域性變數,當然也包括了位元組碼行號指示器、相關Native方法的資訊;注意由於工作記憶體中資料是每個執行緒的私有資料,執行緒間無法相互訪問工作記憶體,因此儲存在工作記憶體的資料不存線上程安全問題。由於同時不同的執行緒間無法訪問對方的工作記憶體,執行緒間的通訊(傳值)必須通過主記憶體來完成。下面是傳值的模型圖:
這裡寫圖片描述

1.2.4主記憶體與工作記憶體的資料儲存型別以及操作方式

儲存資料型別

根據虛擬機器規範,對於一個例項物件中的成員方法而言:

1.區域性變數是基本資料型別(boolean,byte,short,char,int,long,float,double),將直接儲存在工作記憶體(JVM棧的幀棧)中;區域性變數是引用型別,變數的引用會儲存在工作記憶體(JVM棧的幀棧)中,而物件例項將儲存在主記憶體(堆)中
2.成員變數不管它是基本資料型別或者包裝型別(Integer、Double等)還是引用型別,變數引用和例項都會被儲存到主記憶體(堆區)
3.全域性變數(也就是static修飾的變數,也可以稱為類變數)儲存在主記憶體中(方法區中)

操作

主記憶體中的例項物件可以被多執行緒共享,倘若兩個執行緒同時呼叫了同一個物件的同一個方法,那麼兩條執行緒會將要操作的資料拷貝一份到自己的工作記憶體中,執行完成操作後才重新整理到主記憶體,模型圖如下:
這裡寫圖片描述

1.3記憶體模型和記憶體區域劃分的區別

  1. JMM與Java記憶體區域的劃分是不同的概念層次,更恰當說JMM描述的是一組規則,而後者是實際的區域劃分;
  2. JMM與Java記憶體區域唯一相似點,都存在共享資料區域和私有資料區域。在JMM中主記憶體屬於共享資料區域,從某個程度上講應該包括了堆和方法區;而工作記憶體(執行緒私有資料區域),從某個程度上講則應該包括程式計數器、虛擬機器棧以及本地方法棧
  3. 某些地方,我們可能會看見主記憶體被描述為堆記憶體,工作記憶體被稱為執行緒棧,實際上他們表達的都是同一個含義

二.硬體記憶體架構與Java記憶體模型

2.1硬體框架的示意圖

這裡寫圖片描述
上面只是一個示意圖,用來說明大體流程。

注意

1.CPU從記憶體中取資料,然後進行處理,但是記憶體的處理速度遠遠低於CPU,於是在暫存器和記憶體中間加了一個CPU快取,雖小但是速度比記憶體快。
2.暫存器不一定每次都能從快取中取到資料,取不到就去記憶體中直接取。暫存器從快取中取到資料的概率叫做命中率,影響著CPU的執行效能。
3.CPU中處理完資料更新到記憶體中過程是一個相反的過程。

2.2Java執行緒與硬體處理器

2.2.1先來看幾個概念,幫助理解Java執行緒與硬體處理器的關係。

1.程序和執行緒的概念

程序是資源管理的最小單位,執行緒是程式執行的最小單位。

2.Linux 執行緒模型

執行緒由來如下:
Linux最開始沒有執行緒的概念;那麼帶來問題執行效率低;多程序中上下文切換效率低,多執行緒中高,於是有了LinuxThreads;執行緒機制LinuxThreads所採用的就是執行緒-程序”一對一”模型;LinuxThreads 最初的設計相信相關程序之間的上下文切換速度很快,因此每個核心執行緒足以處理很多相關的使用者級執行緒。這就導致了一對一 執行緒模型(即一個使用者執行緒對應一個輕量級程序,而一個輕量級程序對應一個特定的核心執行緒);但仍有明顯缺點,後來有了改進版本也就是NPTL。
NPTL,或稱為 Native POSIX Thread Library,是 Linux 執行緒的一個新實現,它克服了 LinuxThreads 的缺點,同時也符合 POSIX 的需求。與 LinuxThreads 相比,它在效能和穩定性方面都提供了重大的改進。與 LinuxThreads 一樣,NPTL 也實現了一對一的模型(即一個使用者執行緒對應一個輕量級程序,而一個輕量級程序對應一個特定的核心執行緒)。

3.三個主要的概念——核心執行緒、輕量級程序、使用者執行緒

核心執行緒
核心執行緒就是核心的分身,一個分身可以處理一件特定事情。
核心執行緒只能由核心管理並像普通程序一樣被排程。
輕量級程序(Light Weight Process)
輕量級執行緒(LWP)是一種由核心支援的使用者執行緒。
輕量級程序(LWP)是建立在核心之上並由核心支援的使用者執行緒,它是核心執行緒的高度抽象。每一個輕量級程序都與一個特定的核心執行緒關聯。
注意輕量級程序:首先,大多數LWP的操作,如建立、析構以及同步,都需要進行系統呼叫。系統呼叫的代價相對較高:需要在user mode和kernel mode中切換。
其次,每個LWP都需要有一個核心執行緒支援,因此LWP要消耗核心資源(核心執行緒的棧空間)。因此一個系統不能支援大量的LWP。
使用者執行緒
這裡的使用者執行緒指的是完全建立在使用者空間的執行緒庫,使用者執行緒的建立,同步,銷燬,排程完全在使用者空間完成,不需要核心的幫助。因此這種執行緒的操作是極其快速的且低消耗的。

2.2.2JVM中執行緒的實現原理

1.我們在使用Java執行緒時,Java虛擬機器內部是轉而呼叫當前作業系統的核心執行緒來完成當前任務;
2.核心執行緒(Kernel-Level Thread,KLT):它是由作業系統核心(Kernel)支援的執行緒,作業系統核心通過操作排程器進而對執行緒執行排程,並將執行緒的任務對映到各個處理器上。每個核心執行緒可以視為核心的一個分身,這也就是作業系統可以同時處理多工的原因。
3.我們呼叫一個執行緒,呼叫的是使用者空間的執行緒庫,執行緒庫中每個執行緒對應著一個輕量級程序,而一個輕量級程序對應一個核心執行緒,所有的核心執行緒經核心執行緒排程器排程交由CPU完成相應操作。

流程示意圖

這裡寫圖片描述

2.3Java記憶體模型與硬體記憶體架構的關係

1.Java記憶體模型(JMM)只是一個抽象的概念,是一種規則,並不是真正存在的結構;硬體記憶體結構是存在的物理結構。

通過對Java記憶體模型,硬體記憶體架構、以及Java多執行緒的實現原理的瞭解,我們應該已經意識到,多執行緒的執行最終都會對映到硬體處理器上進行執行。對於硬體記憶體來說只有暫存器、快取記憶體、主記憶體的概念,並沒有工作記憶體(執行緒私有資料區域)和主記憶體(堆記憶體)之分,也就是說Java記憶體模型對記憶體的劃分對硬體記憶體並沒有任何影響,因為JMM只是一種抽象的概念,是一組規則,並不實際存在,不管是工作記憶體的資料還是主記憶體的資料,對於計算機硬體來說都會儲存在計算機主記憶體中,當然也有可能儲存到CPU快取或者暫存器中,因此總體上來說,Java記憶體模型和計算機硬體記憶體架構是一個相互交叉的關係,是一種抽象概念劃分與真實物理硬體的交叉。

如下圖

這裡寫圖片描述

三.理解執行緒安全問題的原因

3.1執行緒安全問題

舉例:Thread1和Thread2都想操作主記憶體中的共享變數i,i原來是2。現在Thread1操作完之後想將工作記憶體中的值3更新到主記憶體中,同時Thread2想讀取主記憶體中的i。那麼Thread2讀到的i是2還是3呢。就帶來了不確定性,如果Thread1更新主記憶體的動作先於Thread2讀取主記憶體中資料完成,那麼Thread2中i就是3;如果相反,那麼讀到的就是2。這個不確定性就是執行緒安全問題。

這裡寫圖片描述

3.2Java記憶體模型的承諾

我們來看一下原子性,可見性,有序性

3.2.1原子性

原子性指的是一個操作是不可中斷的,即使是在多執行緒環境下,一個操作一旦開始就不會被其他執行緒影響。
如果不能夠保證原子性操作,在多執行緒環境中就會帶來執行緒安全問題。

3.2.2可見性

在理解可見性之前先看一下指令重排
1.編譯器優化的重排
編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。

2.指令並行的重排
現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性(即後一個執行的語句無需依賴前面執行的語句的結果),處理器可以改變語句對應的機器指令的執行順序

3.記憶體系統的重排
由於處理器使用快取和讀寫快取衝區,這使得載入(load)和儲存(store)操作看上去可能是在亂序執行,因為三級快取的存在,導致記憶體與快取的資料同步存在時間差。

上面第一種屬於編譯器重排,後兩種屬於處理器重排。在多執行緒環境中,這些重排優化可能會導致程式出現記憶體可見性問題,

瞭解更多指令重排可以看這裡
最後看一下可見性概念
可見性指的是當一個執行緒修改了某個共享變數的值,其他執行緒是否能夠馬上得知這個修改的值。
單執行緒中肯定不存在這個問題,因為程式按照序列順序進行。
多執行緒情況下,前面我們分析過,由於執行緒對共享變數的操作都是執行緒拷貝到各自的工作記憶體進行操作後才寫回到主記憶體中的,這就可能存在一個執行緒A修改了共享變數x的值,還未寫回主記憶體時,另外一個執行緒B又對主記憶體中同一個共享變數x進行操作,但此時A執行緒工作記憶體中共享變數x對執行緒B來說並不可見,這種工作記憶體與主記憶體同步延遲現象就造成了可見性問題,另外指令重排以及編譯器優化也可能導致可見性問題,通過前面的分析,我們知道無論是編譯器優化還是處理器優化的重排現象,在多執行緒環境下,確實會導致程式輪序執行的問題,從而也就導致可見性問題,從而帶來執行緒安全問題。

3.2.3有序性

單執行緒中:認為程式碼按照順序依次執行的,是有序的執行。
多執行緒中:指令分配給不同的執行緒執行,並且加上上面指令重排現象,其實就帶來了無序性。也是造成執行緒安全問題的原因。