大郎!快起來看多線程啦!貳


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棧點公眾號!

公眾號二維碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM