大郎!快起來看多執行緒啦!

羅拉快跑跑跑跑發表於2020-11-29

file

一杯茶一包煙,一個Bug改一天!!相信很多“愛碼仕”都曾經對著電腦幾個小時就為改一個bug,最後是在美團小哥指點下修復的。他曾經也是王者,不為別的,就是喜歡送外賣鍛鍊身體還能遠離產品經理和測試。
file
      言歸正傳,本文還是個不正經的多執行緒教程,呃...也算不上教程,個人筆記吧。主要解答一下上文留下的兩個問題:快取一致性協議再詳細說一下JMM(Java Memory Mode),最後再講一下Java物件在堆空間的佈局。等等,哪裡不對,這不是講多執行緒的文章嗎?怎麼沒內味了?稍安勿躁,多執行緒要寫滴,但是寫之前,還是要了解一下更深層次的東西,深入原理,方可百戰不殆,阿彌陀佛~~
file


快取一致性協議

話不多說,我們先拆解一下這7個字。

      快取:不用說了吧,就是為了讓讀更快嘛。有客戶端快取、服務端快取、資料庫快取、本地快取、CDN快取、分散式快取、CPU快取等等等等,而本文主要是針對CPU快取來介紹的,其他快取只要你關注(都黑體加粗了,給個三連吧),我會快馬加鞭的寫。

      一致性:在CPU快取中,這個一致性就是強調在多執行緒併發場景下CPU的本地快取主存中資料的一致性,而這個資料就是指多個執行緒都要用到的共享資料,即我們常說的臨界資源。

      協議:更簡單了,就是認為規定的東西,讓硬體軟體都必須準守的規則,讓它們必須在給定的框框裡工作執行。

      到這裡就拆解完成了,那麼有哪些快取一致性協議呢?它們由什麼來確定的呢?又和我寫CRUD有啥關係呢?
file
      首先,快取一致性協議有很多種,比如:MESI(最常見)、MSI、MOSI、FireFly等等,歡迎大佬們評論區補充。而具體使用哪一種,其實是由CPU架構決定的,也就是說安心寫BUG吧,開發人員無需考慮CPU架構的問題,因為我們有JVM(Java Virtual Machine),遮蔽了平臺間的差異性解決了跨平臺的問題,哪有什麼歲月靜好,不過是有人在負重前行罷了。但是你要知道這些東西,畢竟我們是網際網路的弄潮兒,giao~~~~

接下來還是上圖:
file
      這樣更直觀一些,當CPU1快取行中有A,B且剛好要對其中的A做修改,CPU2也快取了同樣的快取行且對A圖謀不軌。那這時候就需要工程師來制定協議了:讓多顆CPU在同時使用共享資料時,保持資料的一致性,即快取一致性協議。協議型別前邊已經說過了,不同的型別有不同的解決方案。可以通過監聽CPU匯流排的方式實現,也可以在當CPU1修改A時強制其它所有CPU中含有A的快取行同步更新。具體平臺的實現還是看CPU架構

快取行又是個什麼東西?

CPU為了最求極致的程式碼執行效率。當從記憶體中讀取資料時,並不僅僅只讀自己想要的部分。而是讀取足夠的位元組來填入快取記憶體行。快取行的大小通常為2的整數冪,常見的為32位元組和64位元組。

      快取行帶來的是更加高效的資料載入,但同時也帶來了快取行偽共享的問題,還是按上面的圖來說:當CPU1只使用A值,CPU2只使用B值,但是由於快取行的存在且A,B兩個值相鄰,那麼無論哪個CPU修改了自己需要的值,都需要通過匯流排通知對方做更新操作,這樣就影響了效率。解決方案也很簡單: 以64位元組長度快取行為例,在建立A或者B的時候,在值的前後分別補齊7個Long型別的“佔位符”,你問為什麼是7個?因為7個Long型別是7*8=56個位元組,這樣填充之後,無論怎麼載入A,B都不會出現在同一個快取行中,也就規避了偽共享的問題。有關快取行以及偽共享的額詳細介紹請看:https://www.jianshu.com/p/e338b550850fhttps://blog.csdn.net/u010983881/article/details/82704733

最後補充一張我的圖如下:
file
紅色圈代表一個64位元組快取行大小,這樣無論怎麼載入,都不會存在A,B同時被載入到同一快取行中。


JMM詳解

      在上一篇文章中,大概說了JMM是個什麼東西,也丟了一張圖進去。那麼這回我們就再詳細一點介紹下什麼是JMM(Java Memory Mode),還是要強調一下,它是一種抽象層的規範,而且一定要跟Java執行時記憶體空間區分開來,兩者有聯絡,但也有很大區別。再把上節的圖拿過來說吧(有興趣可以掃碼關注不迷路):

image

      一句話來說就是:JMM是一種java虛擬機器規範(看見沒,又是規範,前輩們規定的),其目的是遮蔽掉各種硬體和作業系統的記憶體訪問差異,制定了虛擬機器與計算機記憶體互動要遵循的規章制度,讓我們們工程師兄弟姐妹們安心寫BUG。

      從圖裡可以看出,在JMM的規範中,存在本地工作記憶體主記憶體兩個概念,前者是執行緒私有的後者是執行緒間共享的,此時你是不是想到了JVM裡面的堆、棧、方法區、計數器、常量池等等?沒想到就面壁去(看《深入理解Java虛擬機器》)。沒錯,他們之間存在是有若無的關係,但卻不是一個層次的概念。因為JVM裡面對記憶體空間的劃分是確確實實存在的,而JMM僅僅是抽象規範,指導思想而已。等多執行緒寫完了,再寫JVM的文章,會詳細介紹記憶體區域劃分。
回到主題接著說JMM,還是丟擲以下幾個個問題:

什麼是工作記憶體?

      存放當前方法的所有本地變數資訊,執行緒中的本地變數對其他執行緒是不可見的,不同的執行緒即使用到的是主記憶體中的同一個共享資料,也都只是拷貝一個副本在自己的工作記憶體中做操作,最後重新整理回主存。因此執行緒本地記憶體中的資料是執行緒私有且執行緒安全的(其他執行緒看都看不到能不安全嗎?)。

什麼是主記憶體?

      主要是存放Java的例項物件,也包括了一些共享的類的資訊、常量、靜態變數等,被定義為多執行緒共享的區域。

JMM又是如何規範資料訪問的?

      執行緒的執行離不開資料,主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,JMM定義了八種原子操作來完成。來,我把上面的圖升級一下。然後再看個表格。
file

原子操作 說明
lock(鎖定) 作用於主記憶體變數,標識變數為某個執行緒的獨享狀態
read(讀取) 作用於主記憶體變數,將變數值從主存傳輸到執行緒的工作記憶體中
load(載入) 作用於工作記憶體變數,將read操作得到的變數值放入工作記憶體的變數副本中
use (使用) 作用於工作記憶體變數,把工作記憶體中的一個變數值傳遞給執行引擎
assign(賦值) 作用於工作記憶體變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數
store (儲存) 作用於工作記憶體變數,把工作記憶體中的一個變數值傳送到主記憶體中,以便隨後的write的操作
write (寫入) 作用於主記憶體變數,把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中
unlock(解鎖) 作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出,釋放後的變數才可以被其他執行緒鎖定

      總結一下:前面說的,其實在開發過程中99%的開發人員都用不到,有用得到的大佬,可以留言討論一波。Volatile通過禁止指令重排以及CPU匯流排監聽機制,解決可見性和有序性問題,Synchronized解決了原子性問題,但是其內部還是存在編譯優化的操作,這個後續在Synchronized的專題文章中會詳細介紹。關於JMM更多更深入的文章請看:http://ifeve.com/jmm-cookbook/


Java物件在堆空間的排兵佈陣

      講多執行緒,為啥要說物件在堆空間的排布呢?為了知己知彼,也是為了方便理解Synchronized是如何在底層加鎖的。而且,不管你會不會,面試的時候肯定問,所以學不學呢?哈哈哈哈...

      我們每次新建的物件例項,其實它在堆空間中是被分為三個部分物件頭例項資料以及物件填充(是不是很熟悉?前面在講CPU快取一致性協議的時候有說到快取行對齊)。所以,很多知識學著學著,就都對上了,從很多CPU級別的微觀協議,就能推匯出巨集觀的微服務級別的協議。扯遠了,接著說正題!!!對Java象頭其實又分為了Mark Word、Class Point和陣列長度三部分。

物件頭(Object Head)

      Mark Word這部分資料的大小為64位,其中資料包含HashCode、GC分代年齡、偏向鎖位,鎖標誌位等,如果是偏向鎖還會記錄偏向鎖偏向的執行緒ID。而我們熟知的(如果你還不熟知,可以Google一下,或者等我的文章也行)鎖升級鎖撤銷等等一系列操作,都會在物件頭中找到端倪,狀態都是一一對應的。你說,這些如果滾瓜爛熟了,還會害怕面試官嗎?當然,今天不展開說了,這裡只講佈局。
      Class Point可以理解為就是一個指標,指向描述這個物件型別的class。在64位系統中佔64位,也就是8個位元組,而在32位系統只佔4個位元組。但是為了節省空間(這些研究人員,真是把效能優化到極致,到了我這卻....慘不忍睹啊!!!),在JDK1.6以後預設開啟指標壓縮-XX:+UseCompressedClassPointers,64位系統的也是4位比如問候後邊的Student student = new Student();那麼這個Class Point就是指向的Student.class,因為這個class的內部具體描述了當前這個物件的內部屬性及方法。
      陣列長度(Array Length)這個好理解,就是如果物件是一個陣列物件,那麼這裡儲存的就是陣列的長度。非陣列物件是沒有這塊記憶體區域的,這是在分配記憶體空間的時候就已經確定了的。

例項資料(Instance Data)

這個也簡單,就是你建立的物件真正儲存的資訊,包括自己內部定義的屬性和從父類繼承的屬性。常見的就是一些String、Integer啥的。這個沒啥特別的

物件填充(Padding)

      可以理解為佔位符,還是基於虛擬機器的一些規範,因為Java都是自動記憶體管理,為了方便管理,生成的物件佔用的空間必須為8位元組的整數倍,如果不足整數倍就補空白空間,避免在垃圾回收的時候產生不必要的記憶體空間碎片,增加垃圾回收的壓力。

:以上資料均預設在64位系統中,32位系統,看官們可以自己測一下,虛擬機器均指HotSport
下面用一張圖,展示一下物件在堆空間的排布狀況:
file
嗯~nice,可是口說無憑,我們們上程式碼,在new個阿貓阿狗的看看到底各個部分佔用的空間情況吧。

引入依賴jol-core

openjdk提供的依賴jar,可以協助我們檢視堆中物件各個模組佔用的空間大小

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

上程式碼

/**
 * FileName: JavaObjectMode
 * Author:   RollerRunning
 * Date:     2020/11/28 7:12 PM
 * Description:檢視Java物件在記憶體中的佈局
 */
public class JavaObjectMode {
    public static void main(String[] args) {
        //建立物件
        Student student = new Student();
        // 獲得物件佈局內容
        String s = ClassLayout.parseInstance(student).toPrintable();
        // 列印物件佈局
        System.out.println(s);
    }
}

class Student{
    private String name;
    private String address;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

上結果

大家有興趣也可以CV一下,自己看看。
file
      那結果有了,重點已經圈出來了,分析一波吧?別往下看了,自己先看看,再想十秒鐘。1,2,3,4,5,6,7,8,9,10。好,分析開始!

      第一個圈,圈出來的是物件頭的內容,依次往下是物件的值和一行英文(loss due to the next object alignment)表示對齊填充,增加了一個4位元組的填充,剛好是24位元組,能夠被8整除,滿足了虛擬機器規範,這就是對齊填充價值所在。而後邊的紅圈圈則是當前物件的一個概況,沒啥意義,就是想畫個圈(畫錯了又懶得改而已.....),回到第一個圈圈物件頭,前兩行一共是8位元組,64位的Mark Word。而OFFSET從8開始size為4的那一行就是前面說的Class Point。

      好了,我肝完了,最後賣個關子,請注意一下最後一列的前三行,下一篇文章會根據這三行結合Synchronized關鍵字展開說。

最後,感謝各位觀眾老爺,還請三連!!!
更多文章請掃碼關注或微信搜尋Java棧點公眾號!

公眾號二維碼

相關文章