1. 程式人生 > >大郎!快起來看多執行緒啦!

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

![file](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151852259-573163215.png) --- 一杯茶一包煙,一個Bug改一天!!相信很多“愛碼仕”都曾經對著電腦幾個小時就為改一個bug,最後是在美團小哥指點下修復的。他曾經也是王者,不為別的,就是喜歡送外賣鍛鍊身體還能遠離產品經理和測試。 ![file](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151852786-1679281589.png)       言歸正傳,本文還是個不正經的多執行緒教程,呃...也算不上教程,個人筆記吧。主要解答一下上文留下的兩個問題:**快取一致性協議**再詳細說一下**JMM**(Java Memory Mode),最後再講一下**Java物件在堆空間的佈局**。等等,哪裡不對,這不是講多執行緒的文章嗎?怎麼沒內味了?稍安勿躁,多執行緒要寫滴,但是寫之前,還是要了解一下更深層次的東西,深入原理,方可百戰不殆,阿彌陀佛~~ ![file](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151854103-1145847841.png) --- ### 快取一致性協議 話不多說,咱先拆解一下這7個字。       快取:不用說了吧,就是為了讓讀更快嘛。有客戶端快取、服務端快取、資料庫快取、本地快取、CDN快取、分散式快取、CPU快取等等等等,而本文主要是針對**CPU快取**來介紹的,其他快取只要你**關注**(都黑體加粗了,給個三連吧),我會快馬加鞭的寫。       一致性:在CPU快取中,這個一致性就是強調在多執行緒併發場景下**CPU的本地快取**和**主存**中資料的一致性,而這個資料就是指多個執行緒都要用到的共享資料,即我們常說的臨界資源。       協議:更簡單了,就是認為規定的東西,讓硬體軟體都必須準守的規則,讓它們必須在給定的框框裡工作執行。       到這裡就拆解完成了,那麼有哪些快取一致性協議呢?它們由什麼來確定的呢?又和我寫CRUD有啥關係呢? ![file](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151855317-853855008.png)       首先,快取一致性協議有很多種,比如:MESI(最常見)、MSI、MOSI、FireFly等等,歡迎大佬們評論區補充。而具體使用哪一種,其實是由**CPU架構**決定的,也就是說安心寫BUG吧,開發人員無需考慮CPU架構的問題,因為我們有**JVM(Java Virtual Machine)**,遮蔽了平臺間的差異性解決了跨平臺的問題,哪有什麼歲月靜好,不過是有人在負重前行罷了。但是你要知道這些東西,畢竟我們是網際網路的弄潮兒,giao~~~~ 接下來還是上圖: ![file](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151855956-146845703.png)       這樣更直觀一些,當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/e338b550850f 和 https://blog.csdn.net/u010983881/article/details/82704733 最後補充一張我的圖如下: ![file](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151857453-469310379.png) 紅色圈代表一個64位元組快取行大小,這樣無論怎麼載入,都不會存在A,B同時被載入到同一快取行中。 --- ### JMM詳解       在上一篇文章中,大概說了JMM是個什麼東西,也丟了一張圖進去。那麼這回我們就再詳細一點介紹下什麼是JMM(Java Memory Mode),還是要強調一下,它是一種抽象層的規範,而且一定要跟Java執行時記憶體空間區分開來,兩者有聯絡,但也有很大區別。再把上節的圖拿過來說吧(有興趣可以掃碼關注不迷路): ![image](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151857945-235428153.png)       一句話來說就是:**JMM是一種java虛擬機器規範**(看見沒,又是規範,前輩們規定的),其目的是**遮蔽**掉各種硬體和作業系統的記憶體訪問差異,制定了虛擬機器與計算機記憶體互動要遵循的規章制度,讓咱們工程師兄弟姐妹們安心寫BUG。       從圖裡可以看出,在JMM的規範中,存在本地**工作記憶體**和**主記憶體**兩個概念,前者是執行緒私有的後者是執行緒間共享的,此時你是不是想到了JVM裡面的堆、棧、方法區、計數器、常量池等等?沒想到就面壁去(看《深入理解Java虛擬機器》)。沒錯,他們之間存在是有若無的關係,但卻不是一個層次的概念。因為JVM裡面對記憶體空間的劃分是確確實實存在的,而JMM僅僅是抽象規範,指導思想而已。等多執行緒寫完了,再寫JVM的文章,會詳細介紹記憶體區域劃分。 回到主題接著說JMM,還是丟擲以下幾個個問題: #### 什麼是工作記憶體?       存放當前方法的所有本地變數資訊,執行緒中的本地變數對其他執行緒是不可見的,不同的執行緒即使用到的是主記憶體中的同一個共享資料,也都只是拷貝一個副本在自己的工作記憶體中做操作,最後重新整理回主存。因此執行緒本地記憶體中的資料是執行緒私有且執行緒安全的(其他執行緒看都看不到能不安全嗎?)。 #### 什麼是主記憶體?       主要是存放Java的例項物件,也包括了一些共享的類的資訊、常量、靜態變數等,被定義為多執行緒共享的區域。 #### JMM又是如何規範資料訪問的?       執行緒的執行離不開資料,主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,JMM定義了八種原子操作來完成。來,我把上面的圖升級一下。然後再看個表格。 ![file](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151858692-818513661.png) 原子操作 | 說明 ---|--- 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](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151859127-646704854.png) 嗯~nice,可是口說無憑,咱們上程式碼,在new個阿貓阿狗的看看到底各個部分佔用的空間情況吧。 #### 引入依賴jol-core openjdk提供的依賴jar,可以協助我們檢視堆中物件各個模組佔用的空間大小 ``` ``` #### 上程式碼 ``` /** * 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](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151900052-575281678.png)       那結果有了,重點已經圈出來了,分析一波吧?別往下看了,自己先看看,再想十秒鐘。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棧點**公眾號! ![公眾號二維碼](https://img2020.cnblogs.com/other/2120441/202011/2120441-20201129151900447-975102