多線程併發-java內存模型和計算機基礎

CPU緩存一致性協議MESI

CPU在摩爾定律的指導下以每18個月翻一番的速度在發展,然而內存和硬盤的發展速度遠遠不及CPU。這就形成了高性能能的內存和硬盤價格及其昂貴。然而CPU的高度運算須要高速的數據。爲了解決這個問題,CPU廠商在CPU中內置了少許的高速緩存以解決I\O速度和CPU運算速度之間的不匹配問題。 在CPU訪問存儲設備時,不管是存取數據抑或存取指令,都趨於彙集在一片連續的區域中,這就被稱爲局部性原理。java

  1. 時間局部性(Temporal Locality):若是一個信息項正在被訪問,那麼在近期它極可能還會被再次訪問。 好比循環、遞歸、方法的反覆調用等。
  2. 空間局部性(Spatial Locality):若是一個存儲器的位置被引用,那麼未來他附近的位置也會被引用。 好比順序執行的代碼、連續建立的兩個對象、數組等
帶有高速緩存的CPU執行計算的流程
  1. 程序以及數據被加載到主內存
  2. 指令和數據被加載到CPU的高速緩存
  3. CPU執行指令,把結果寫到高速緩存
  4. 高速緩存中的數據寫回主內存
多核CPU多級緩存一致性協議MESI

MESI協議緩存狀態數組

緩存行(Cache line):緩存存儲數據的單元。緩存

狀態 描述 監放任務
M 修改 (Modified) 該Cache line有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中。 緩存行必須時刻監聽全部試圖讀該緩存行相對就主存的操做,這種操做必須在緩存將該緩存行寫回主存並將狀態變成S(共享)狀態以前被延遲執行。
E 獨享、互斥 (Exclusive) 該Cache line有效,數據和內存中的數據一致,數據只存在於本Cache中。 緩存行也必須監聽其它緩存讀主存中該緩存行的操做,一旦有這種操做,該緩存行須要變成S(共享)狀態。
S 共享 (Shared) 該Cache line有效,數據和內存中的數據一致,數據存在於不少Cache中。 緩存行也必須監聽其它緩存使該緩存行無效或者獨享該緩存行的請求,並將該緩存行變成無效(Invalid)。
I 無效 (Invalid) 該Cache line無效。

注意: 對於M和E狀態而言老是精確的,他們在和該緩存行的真正狀態是一致的,而S狀態多是非一致的。若是一個緩存將處於S狀態的緩存行做廢了,而另外一個緩存實際上可能已經獨享了該緩存行,可是該緩存卻不會將該緩存行升遷爲E狀態,這是由於其它緩存不會廣播他們做廢掉該緩存行的通知,一樣因爲緩存並無保存該緩存行的copy的數量,所以(即便有這種通知)也沒有辦法肯定本身是否已經獨享了該緩存行。多線程

從上面的意義看來E狀態是一種投機性的優化:若是一個CPU想修改一個處於S狀態的緩存行,總線事務須要將全部該緩存行的copy變成invalid狀態,而修改E狀態的緩存不須要使用總線事務。併發

cpu多級緩存 - 亂序執行優化

處理器爲提升運算速度兒作出違背代碼原有順序的優化。性能

在單核處理器時代處理器的亂序執行優化不會影響執行結果。在多核處理中,某個核心執行寫入操做時,將某個標誌當作寫入完成,進行重排優化,可能會先執行標誌指令致使其餘核心覺得改核心已經執行完成寫入操做。從而拿到錯誤的值。優化

java內存模型(java memory model, JMM)

  1. 堆heap 堆是運行時肯定的內存,由java GC來維護大小,優勢是能夠動態的肯定大小,缺點是運行時動態肯定內存因此速度相對棧小一點。對象存放在堆上。靜態變量跟隨類一塊兒存放在堆上。
  2. 棧stack 棧內存的速度相對堆內存更快,僅次於寄存器,缺點是大小必須是編譯期肯定的。缺少必定的靈活性,存放一些基本的數據變量(int double。。。)java內存要求本地變量(Local Variable),調用棧必須存放在線程棧(Thead Stack)中。

本地變量可能存放的是對象的引用。當兩個線程同時引用一個對象時,那麼這兩個線程的本地引用存放的是這個對象的私有拷貝。線程

硬件內存模型如圖 3d

硬件內存模型和java內存模型的對應模型如圖:

java內存抽象模型結構

看圖,本地內存:本地內存是java抽象的概念,涵蓋了緩存,寫緩存區,寄存器,其餘硬件和編譯器優化。本地內存儲存了共享變量的副本,從硬件的角度上講主內存就是硬件內存,可是爲了獲取更好的速度,java可能會將數據存儲在寄存器或者高速緩存區。若是線程要通訊必需要通過主內存,流程是先在主內存中獲取共享變量,存儲在本地內存中經由進程計算,而後刷新至主內存,再經由其餘線程訪問。

java內存模型- 同步操做與規則

  1. lock和Unlack:做用在主內存上只有在Unlock的狀況下內存才能夠被其餘線程鎖定。
  2. Read:做用在主內存上,把主內存中的變量輸送在工做內存中。
  3. Load:做用工做內存中,把主內存中的值放入到工做內存副本中。
  4. use:做用於工做內存,把數據給執行引擎。每當執行器須要使用到變量時或者執行字節碼指令時會執行這個操做。
  5. assign:賦值,在執行賦值操做時執行,將執行引擎中的值賦值給工做內存。
  6. store:存儲,把工做內存中的值傳遞到主內存中。
  7. write:寫入,將工做內存中的值寫入到主內存中。

下面介紹一下規則,規則是用來限制每一步是如何操做的。cdn

  1. 不容許read和load、store和write單一出現,由於他們是一個連貫的操做。並且必須是按順序執行的。load必須是read以後,write必須是store以後,可是不必定是連續操做,在他們之間能夠插入其餘的指令。
  2. 不容許線程丟棄assign操做,也就是說執行完了以後必須放入工做內存中。
  3. 不容許線程不通過Assign操做直接把數據給主內存。
  4. 一個新的變量只能在主內存中誕生。
  5. 一個變量只容許一個線程對其lack操做,可是能夠被一個線程lack屢次,lack屢次以後只有執行相同次數的unlack才能被解鎖。
  6. 若是一個變量執行了lack操做以後將會清楚工做內存中該變量的值。執行引擎在使用變量時須要從新執行read-load-use等操做。
  7. 若是沒有執行一個lack操做的變量不能執行unlack操做。或者被其餘線程執行了lack操做的線程也不能被改線程執行unlack。
多線程併發的優點和缺點