Java多線程內存模型

JMM的基本概念

Java做爲平臺無關性語言,JLS(Java語言規範)定義了一個統一的內存管理模型JMM(Java Memory Model)。JMM規定了jvm內存分爲主內存和工做內存 ,主內存存放程序中全部的類實例、靜態數據等變量,是多個線程共享的,而工做內存存放的是該線程從主內存中拷貝過來的變量以及訪問方法所取得的局部變量,是每一個線程私有的其餘線程不能訪問。每一個線程對變量的操做都是以先從主內存將其拷貝到工做內存再對其進行操做的方式進行,多個線程之間不能直接互相傳遞數據通訊,只能經過共享變量來進行。程序員

從上圖來看,線程1與線程2之間如要通訊的話,必需要經歷下面2個步驟:面試

1. 首先,線程1把本地工做內存中更新過的共享變量刷新到主內存中去。數據庫

2. 而後,線程2到主內存中去讀取線程1以前已更新過的共享變量。多線程

典型的高併發引發的問題就存在因爲線程讀取到的數據尚未從另外的線程刷新到主內存中而引發的數據不一致問題。架構

主內存與工做內存的數據交互

JLS一共定義了8種操做來完成主內存與線程工做內存的數據交互:併發

1. lock:把主內存變量標識爲一條線程獨佔,此時不容許其餘線程對此變量進行讀寫jvm

2. unlock:解鎖一個主內存變量分佈式

3. read:把一個主內存變量值讀入到線程的工做內存高併發

4. load:把read到變量值保存到線程工做內存中做爲變量副本學習

5. use:線程執行期間,把工做內存中的變量值傳給字節碼執行引擎

6. assign:字節碼執行引擎把運算結果傳回工做內存,賦值給工做內存中的結果變量

7. store:把工做內存中的變量值傳送到主內存

8. write:把store傳送進來的變量值寫入主內存的變量中

使用標準的操做再來重現一下上方的2個線程之間的交互流程則是這樣的:

  1. 線程1從主內存read一個值爲0的變量x到工做內存
  2. 使用load把變量x保存到工做內存做爲變量副本
  3. 將變量副本x使用use傳遞給字節碼執行引擎進行x++操做
  4. 字節碼執行引擎操做完畢後使用assign將結果賦值給變量副本
  5. 使用store把變量副本傳送到主內存
  6. 使用write把store傳送的數據寫到主內存
  7. 線程2從主內存read到x,而後load-->use-->assign-->store-->write

另外使用這8種操做也有一些規則:

  1. read 和 load必須以組合的方式出現,不容許一個變量從主內存讀取了但工做內存不接受狀況出現
  2. store和write必須以組合的方式出現,不容許從工做內存發起了存儲操做但主內存不接受的狀況出現
  3. 工做內存的變量若是沒有通過 assign 操做,不容許將此變量同步到主內存中
  4. 在 use 操做以前,必須通過 load 操做
  5. 在 store 操做以前,必須通過 assign 操做
  6. unlock 操做只能做用於被 lock 操做鎖定的變量
  7. 一個變量被執行了多少次 lock 操做就要執行多少次 unlock 才能解鎖
  8. 一個變量只能在同一時刻被一條線程進行 lock 操做
  9. 執行 lock 操做後,工做內存的變量的值會被清空,須要從新執行 load 或 assign 操做初始化變量的值
  10. 對一個變量執行 unlock 操做以前,必須先把此變量同步回主內存中

多線程中的原子性、可見性、有序性

1. 原子性:關於原子性的定義能夠參考個人上篇文章《淺談數據庫事務》。在JLS中保證原子性的操做包括read、load、assign、use、store和write。基本數據類型(除了long 和double)操做都具備原子性。

若是須要更大範圍的原子性操做的時候,可使用lock和unlock操做來完成這種需求。

2. 可見性:是指當一個線程修改了共享變量的值,其餘線程是否可以當即得知這個修改。

由上方JMM的概念得知,線程操做數據是在工做內存的,當多個線程操做同一個數據的時候很容易讀取到尚未被write到主內存變量的值。

Java是如何保證可見性的:volatile、synchronized、final關鍵字

3. 有序性:在併發時,程序的執行可能會出現亂序。給人的直觀感受就是:寫在前面的代碼,會在後面執行。有序性問題的緣由是由於程序在執行時,可能會進行指令重排,重排後的指令與原指令的順序未必一致。關於指令重排會在下方講。

指令重排

int a=1;
int b=2;
int c=3;
int d=4;

你能說出上方這段代碼的執行順序麼?其實咱們可能理所固然的覺得它會從上往下順序執行。事實上,在實際運行時,爲了優化指令的執行順序等,代碼指令可能並非嚴格按照代碼語句順序執行的。上方的代碼執行順序可能徹底反過來,這個就是指令重排。

不過呢,指令重排也不是能夠隨意重排的,它須要遵照必定的規則:

1. 程序順序規則:一個線程內保證語義的正確性。

2. 鎖規則:解鎖確定先於隨後的加鎖前。

3. volatile規則:對一個volatile的寫,先於volatile的讀。

4. 傳遞性:若是A 先於 B,且B 先於 C,那麼A 確定先於 C。

5. start()規則:線程的start()操做先於線程的其餘操做。

6. join()規則:線程的全部操做先於線程的關閉。

7. 程序中斷規則:線程的中斷先於被中斷後執行的代碼。

8. 對象finalize規則:一個對象的初始化完成先於finalize()方法。

volatile關鍵字

volatile關鍵字旨在告訴虛擬機在這個地方要注意不能隨意的進行指令重排,而虛擬機看到一個變量被volatile修飾之後就會採用一些特殊的手段來保證變量的可見性。不過要注意的是volatile關鍵字不能保證原子性。


針對於上面所涉及到的知識點我總結出了有1到5年開發經驗的程序員在面試中涉及到的絕大部分架構面試題及答案作成了文檔和架構視頻資料免費分享給你們(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術資料),但願能幫助到您面試前的複習且找到一個好的工做,也節省你們在網上搜索資料的時間來學習,也能夠關注我一下之後會有更多幹貨分享。

資料獲取方式: QQ羣搜索「708-701-457」 便可免費領取



相關文章
相關標籤/搜索