天天看點

java記憶體模型《深入了解java虛拟機》

前言

首先我們來捋一下java記憶體模型(JMM)的由來,java記憶體模型是Java虛拟機規範為了屏蔽掉各種硬體和作業系統通路記憶體差異而制定的一種模型。

那麼為什麼要定義記憶體模型?其實上面的定義已經給了答案,就是為了實作記憶體屏蔽。那麼接着問,為什麼要實作記憶體屏蔽?因為要實作并發。

為什麼要實作并發?實作并發的措施是什麼?實作并發所帶來的問題是什麼?

首先回答下後面兩個問題,首先實作并發的措施是多線程,其次實作并發後帶來的問題是資料一緻性無法保證,就是我們常說的線程安全。上面兩個問題很多java程式員都知道的,但是其實對于大多數程式員來說為什麼要實作并發是很難回答的,這裡給出我認為的兩個點:

  1. cpu的運算能力太強,單線程無法發揮出cpu強大的運算能力,導緻cpu的資源有很大的浪費,這裡的cpu運算能力的強大是相比較網絡通信、IO操作及資料庫通路的
  2. 服務端應用要提高TPS支援并發是一個不錯的選擇

到此我們簡單總結下:java記憶體模型(JMM)是JVM為了實作多線程而做的記憶體屏蔽的一個限制。

java記憶體模型要做什麼?

java記憶體模型的目的是定義程式中變量的通路規則,這個變量不是特指我們代碼裡的變量,但是可以類比我們的成員變量,JVM在定義變量通路規則的時候是圍繞着在并發過程中如何處理原子性、可見性和有序性這三個特征來建立的。下面我們來看下這三個特征:

原子性(Atomicity):由Java記憶體模型來直接保證的原子性變量包括read、load、assign、use、store和write,我們大緻可以認為基本資料類型的通路讀寫是具備原子性的。如果應用場景需要一個更大方位的原子性保證,Java記憶體模型還提供了lock和unlock操作來滿足這種需求,盡管虛拟機未把lock和unlock操作直接開放給使用者使用,但是卻提供了更高層次的位元組碼指令monitorenter和monitorexit來隐式的使用這兩個操作,這兩個位元組碼指令反應到Java代碼中就是同步塊–synchronized關鍵字,是以在synchronized塊之間的操作也具備原子性。

可見性(Visibility):可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。上文在講解volatile變量的時候我們已詳細讨論過這一點。Java記憶體模型是通過在變量修改後将新值同步回主記憶體,在變量讀取前從主記憶體重新整理變量值這種依賴主記憶體作為傳遞媒介的方式來實作可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的差別是,volatile的特殊規則保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理。是以,可以說volatile保證了多線程操作時變量的可見性,而普通變量則不能保證這一點。除了volatile之外,Java還有兩個關鍵字能實作可見性,即synchronized和final.同步快的可見性是由“對一個變量執行unlock操作前,必須先把此變量同步回主記憶體”這條規則獲得的,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,并且構造器沒有把"this"的引用傳遞出去,那麼在其他線程中就能看見final字段的值。

有序性(Ordering):Java記憶體模型的有序性在前面講解volatile時也詳細的讨論過了,Java程式中天然的有序性可以總結為一句話:如果在本線程内觀察,所有的操作都是有序的:如果在一個線程中觀察另外一個線程,所有的線程操作都是無序的。前半句是指“線程内表現為串行的語義”,後半句是指“指令重排序”現象和“工作記憶體與主記憶體同步延遲”現象。Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一個時刻隻允許一條線程對其進行lock操作”這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊隻能串行的進入。

java記憶體模型做了什麼?

java記憶體模型将記憶體劃分為了主記憶體和工作記憶體,以及定義了主記憶體和工作記憶體的互動規範。

主記憶體

java虛拟機規定所有的變量(不是程式中的變量)都必須在主記憶體中産生,為了友善了解,可以認為是堆區。

工作記憶體

java虛拟機中每個線程都有自己的工作記憶體,該記憶體是線程私有的為了友善了解,可以認為是虛拟機棧。虛拟機規定,線程對主記憶體變量的修改必須線上程的工作記憶體中進行,不能直接讀寫主記憶體中的變量。不同的線程之間也不能互相通路對方的工作記憶體。如果線程之間需要傳遞變量的值,必須通過主記憶體來作為中介進行傳遞。

簡單總結下主記憶體和工作記憶體:主記憶體的資料是線程共享的,變量必須在主記憶體産生,線程隻能操作自己的工作記憶體,不同線程的工作記憶體不能互相通信。下面看下主記憶體和工作記憶體是如何互動的

主記憶體和工作記憶體的互動

實體機高速緩存和主記憶體之間的互動有協定,同樣的,java記憶體中線程的工作記憶體和主記憶體的互動是由java虛拟機定義了如下的8種操作來完成的,每種操作必須是原子性的(double和long類型在某些平台有例外,參考volatile詳解和非原子性協定)

java虛拟機中主記憶體和工作記憶體互動,就是一個變量如何從主記憶體傳輸到工作記憶體中,如何把修改後的變量從工作記憶體同步回主記憶體。

  • lock(鎖定):作用于主記憶體的變量,一個變量在同一時間隻能一個線程鎖定,該操作表示這條線成獨占這個變量
  • unlock(解鎖):作用于主記憶體的變量,表示這個變量的狀态由處于鎖定狀态被釋放,這樣其他線程才能對該變量進行鎖定
  • read(讀取):作用于主記憶體變量,表示把一個主記憶體變量的值傳輸到線程的工作記憶體,以便随後的load操作使用
  • load(載入):作用于線程的工作記憶體的變量,表示把read操作從主記憶體中讀取的變量的值放到工作記憶體的變量副本中(副本是相對于主記憶體的變量而言的)
  • use(使用):作用于線程的工作記憶體中的變量,表示把工作記憶體中的一個變量的值傳遞給執行引擎,每當虛拟機遇到一個需要使用變量的值的位元組碼指令時就會執行該操作
  • assign(指派):作用于線程的工作記憶體的變量,表示把執行引擎傳回的結果指派給工作記憶體中的變量,每當虛拟機遇到一個給變量指派的位元組碼指令時就會執行該操作
  • store(存儲):作用于線程的工作記憶體中的變量,把工作記憶體中的一個變量的值傳遞給主記憶體,以便随後的write操作使用
  • write(寫入):作用于主記憶體的變量,把store操作從工作記憶體中得到的變量的值放入主記憶體的變量中

如果要把一個變量從主記憶體傳輸到工作記憶體,那就要順序的執行read和load操作,如果要把一個變量從工作記憶體回寫到主記憶體,就要順序的執行store和write操作。對于普通變量,虛拟機隻是要求順序的執行,并沒有要求連續的執行,是以如下也是正确的。對于兩個線程,分别從主記憶體中讀取變量a和b的值,并不一樣要read a; load a; read b; load b; 也會出現如下執行順序:read a; read b; load b; load a; (對于volatile修飾的變量會有一些其他規則,後邊會詳細列出),對于這8中操作,虛拟機也規定了一系列規則,在執行這8中操作的時候必須遵循如下的規則:

  • 不允許read和load、store和write操作之一單獨出現,也就是不允許從主記憶體讀取了變量的值但是工作記憶體不接收的情況,或者不允許從工作記憶體将變量的值回寫到主記憶體但是主記憶體不接收的情況
  • 不允許一個線程丢棄最近的assign操作,也就是不允許線程在自己的工作線程中修改了變量的值卻不同步/回寫到主記憶體
  • 不允許一個線程回寫沒有修改的變量到主記憶體,也就是如果線程工作記憶體中變量沒有發生過任何assign操作,是不允許将該變量的值回寫到主記憶體
  • 變量隻能在主記憶體中産生,不允許在工作記憶體中直接使用一個未被初始化的變量,也就是沒有執行load或者assign操作。也就是說在執行use、store之前必須對相同的變量執行了load、assign操作
  • 一個變量在同一時刻隻能被一個線程對其進行lock操作,也就是說一個線程一旦對一個變量加鎖後,在該線程沒有釋放掉鎖之前,其他線程是不能對其加鎖的,但是同一個線程對一個變量加鎖後,可以繼續加鎖,同時在釋放鎖的時候釋放鎖次數必須和加鎖次數相同。
  • 對變量執行lock操作,就會清空工作空間該變量的值,執行引擎使用這個變量之前,需要重新load或者assign操作初始化變量的值
  • 不允許對沒有lock的變量執行unlock操作,如果一個變量沒有被lock操作,那也不能對其執行unlock操作,當然一個線程也不能對被其他線程lock的變量執行unlock操作
  • 對一個變量執行unlock之前,必須先把變量同步回主記憶體中,也就是執行store和write操作

    當然,最重要的還是如開始所說,這8個動作必須是原子的,不可分割的。

    針對volatile修飾的變量,會有一些特殊規定。

volatile修飾的變量的特殊性

  1. volatile修飾的變量對所有線程可見,換句話說volatile修飾的變量的值一旦改變會立馬重新整理到主記憶體,而且其他線程的工作記憶體在使用volatile變量時也必須重新加載主記憶體的新值,總結volatile是最輕量級的同步實作,但是volatile無法保證線程安全,原因是java代碼不是原子性的,比如一行代碼可能會被編譯成多行位元組碼,同樣一行位元組碼也可能被JIT編譯成多行機器碼,是以volatile無法實作線程同步,經典案例i++;
  2. volatile禁止指令重排序

同一個線程内觀察所有指令都是有序的,一個線程内觀察另一個線程的所有操作指令都是無序的

這句話怎麼了解呢,就是在并發的情況下我們所寫的代碼都是無序,那麼我們怎麼保證我們程式的執行順序,用volatile,還有synchronized的,但是這樣的話我們的程式寫起來就會很複雜,對于這種情況jvm提出了happens-before,happens-before翻譯成中文是先行發生。

先行發生原則

先行發生原則是Java記憶體模型中定義的兩個操作之間的偏序關系。比如說操作A先行發生于操作B,那麼在B操作發生之前,A操作産生的“影響”都會被操作B感覺到。這裡的影響是指修改了記憶體中的共享變量、發送了消息、調用了方法等。個人覺得更直白一些就是有可能對操作B的結果有影響的都會被B感覺到,對B操作的結果沒有影響的是否感覺到沒有太大關系。

Java記憶體模型自帶先行發生原則有哪些

  • 程式次序原則

    在一個線程内部,按照代碼的順序,書寫在前面的先行發生與後邊的。或者更準确的說是在控制流順序前面的先行發生與控制流後面的,而不是代碼順序,因為會有分支、跳轉、循環等。

  • 管程鎖定規則

    一個unlock操作先行發生于後面對同一個鎖的lock操作。這裡必須注意的是對同一個鎖,後面是指時間上的後面

  • volatile變量規則

    對一個volatile變量的寫操作先行發生與後面對這個變量的讀操作,這裡的後面是指時間上的先後順序

  • 線程啟動規則

    Thread對象的start()方法先行發生與該線程的每個動作。當然如果你錯誤的使用了線程,建立線程後沒有執行start方法,而是執行run方法,那此句話是不成立的,但是如果這樣其實也不是線程了

  • 線程終止規則

    線程中的所有操作都先行發生與對此線程的終止檢測,可以通過Thread.join()和Thread.isAlive()的傳回值等手段檢測線程是否已經終止執行

  • 線程中斷規則

    對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測到是否有中斷發生。

  • 對象終結規則

    一個對象的初始化完成先行發生于他的finalize方法的執行,也就是初始化方法先行發生于finalize方法

  • 傳遞性

    如果操作A先行發生于操作B,操作B先行發生于操作C,那麼操作A先行發生于操作C。

簡單總結下先行發生原則:先行發生與時間上的先後無關,注重的是影響,也就是先發生的操作的結果對後發生的行為可見。

總結

最後做一個總結,本文的主題是java記憶體模型,首先講了Java記憶體模型是為了并發設計的,然後介紹了java記憶體模型圍繞的原子性、可見性及有序性三大原則,接着介紹了主記憶體和工作記憶體及互動方式,緊接着介紹了volatile關鍵字,最後介紹的是先行發生原則。

掃碼關注公衆号

java記憶體模型《深入了解java虛拟機》