全網最硬核Java程序員必備底層知識(一)

愛敲代碼的小黃 2022-01-07 05:53:38 阅读数:663

最硬 硬核 java 程序

你好,我是小黃,一名獨角獸企業的Java開發工程師。 感謝茫茫人海中我們能够相遇,
俗話說:當你的才華和能力,不足以支撐你的夢想的時候,請靜下心來學習
希望優秀的你可以和我一起學習,一起努力,實現屬於自己的夢想。

在這裏插入圖片描述

一、引言

對於Java開發者而言,關於底層知識,我們一般當做黑盒來進行使用,不需要去打開這個黑盒。

但隨著目前程序員行業的發展,我們有必要打開這個黑盒,去探索其中的奧妙。

本篇系列文章,將帶你一起探索底層黑盒的奧秘之處。

二、相關書籍推薦

讀書的原則:不求甚解,觀其大略

你如果進到廬山裏頭,二話不說,蹲下頭來,彎下腰,就對著某棵樹某棵小草猛研究而不是說先把廬山的整體脈絡研究清楚了,那麼你的學習方法肯定效率巨低而且特別痛苦。

最重要的還是慢慢地打擊你的積極性,說我的學習怎麼那麼不happy啊,怎麼那麼沒勁那,因為你的學習方法錯了,大體讀明白,先拿來用,用著用著,很多道理你就明白了。

  • 《編碼:隱匿在計算機軟硬件背後的語言》
  • 《深入理解計算機系統》(不建議讀)
  • 《算法導論》、《Java數據結構和算法》、《劍指offer》
  • 《30天自制操作系統》
  • 《TCP/IP詳解》卷一
  • 龍書《編譯原理》

三、硬件基礎知識

1、CPU的制作過程

CPU是如何制作的?

我相信每一個人都會都這麼一個問號,今天來告訴你,所有的CPU都來源於:沙子

對於CPU的制作流程,這裏有篇文章,大家有興趣的可以看一下:CPU是如何制作的

沒有興趣的直接看概括:

  • 第一步:我們從沙子中提供單晶矽 晶體
  • 第二步:將晶體切割成薄片,得到 晶圓
  • 第三步:將金屬粒子轟擊到晶圓上,再進行 電鍍
  • 第四步:在晶圓上進行 光刻,完成不同晶體管之間的導線互連
  • 第五步:質量檢測,去除質量差的CPU

2、CPU的原理

計算機最開始要解决的問題:如何代錶數字?

最原始的計算機采用的是利用燈泡,當我們計算 0100 + 1010 時,我們使用 8 個燈泡,以 燈泡的狀態來代錶 01,這樣我麼的第一版的計算機已經OK了。

  • ENIAC重達27噸,占地1800平方英尺(約合167.2平方米)。誕生於二戰時期,最初是作為輔助炮兵計算炮彈軌迹的工具。
    在這裏插入圖片描述

第一版的計算機,有一個讓人哭笑不得的缺點。

我們在對計算機進行高速計算時,燈泡的閃光頻次比較高,有可能造成燈泡的損壞,就需要有工作人員及時的更換燈泡,從而影響效率。

作為最初的一款計算機來說,他的面世足够讓地球人震撼。

目前的計算機大都采取晶體管的方式進行計算,利用 與門或門非門或非門 的狀態去錶示不同的計算方式。

我們經常在生活中聽說,CPU32比特、CPU64比特,簡單來說,他們的區別就是:一次性讀取多少比特(bit)的數字

我們任何的計算,都可以通過邏輯運算來進行得到,我們來看下面這個邏輯運算:0 && 1 = 0,是怎麼實現的?

首先,我們看一下電路圖:這是一個 與門 電路圖
在這裏插入圖片描述
對於該電路而言,AB 作為輸入,Q 作為輸出。

例如 A 輸入低電平、B 輸出高電平,那麼 Q 就會輸出低電平,轉換為二進制就是 A 輸入0、B 輸出1,那麼 Q 就會輸出0,對應的邏輯運算錶達式為 0 && 1 = 0

這裏有一個關於BUG來源的小故事:從前有一個人,進行計算機的計算時,發現數字總是不正確,找了好久也沒找到原因,後來發現是計算機有個孔被蟲子(BUG)腐蝕了,導致沒辦法進行低電平、高電平的切換,從此,我們編程上的錯誤就叫做:BUG

3、匯編語言的執行過程

我們想一下,在上面電路中,發生了 0 && 1 = 0 這樣的事件,我使用者怎麼知道機器發生了這種事件呢?

我們不可能直接把機器拆開,看裏面的電平變換吧。

所以,在這裏就出現了 匯編語言,而匯編語言的本質也是作為機器語言的助記符 出現的

比如,我們給計算機說,你去給我計算 1 + 2 這個操作,計算機需要進行高低電平的差异輸出計算結果

而我們的匯編語言:movaddsub

我們可以一目了然的了解目前計算機的操作狀態

計算機的組成圖:
在這裏插入圖片描述

我們看一下計算機的計算的整個流程:
在這裏插入圖片描述

這裏要說明一下 JavaC 語言的區別:

  • C語言:直接可以讓CPU進行編譯
  • Java語言:需要讓 JVM 翻譯,才能讓 CPU 編譯,這也正是 Java 跨平臺的關鍵所在

4、量子計算機

對於量子計算機而言,目前世界上都在進行探索,暫無成果

在我們普通的計算機中,一個比特代錶 1 或者 0,32個比特,可以代錶 2^32 的任何一個數字

而我們的量子比特,最亮眼地方在於,他可以同時錶示10

  • 一比特量子比特:1、0
  • 二比特量子比特:00、01、10、11
  • 三比特量子比特:000、001、011、111…
  • 三十二比特量子比特:一次性錶示 2 ^ 32 的數字

這樣描述可能不太直觀,我們看一個例子:

現在有一個數字,我們知道該數字範圍為: 1~2^32,我們怎麼能快速求出該數字呢?

對於普通比特而言,一次只能錶示一個,所以我們需要循環遍曆 2^32 次,才可以找到該數字

而對於量子比特而言,直接使用 32 比特的操作系統即可完成

5、CPU的基本組成

在這裏插入圖片描述

  • PC(Programme Counter):程序技術器當前指令的地址
  • Registers:寄存器,暫時存儲CPU計算需要的數據
  • ALU(Arithmetic & logic Unit):運算單元,做運算使用
  • CU(Control Unit):控制單元
  • MMU(memory Mangagement unit):內存處理單元
  • Cache:緩存

5.1 ALU

在這裏插入圖片描述
之前的CPU屬於單核情况,這樣的話,會只有一個 Registers,我們的 PC 會不斷的進行切換來指向新的線程,將所對應的數據存放到 Registers,對於切換(context switch)而言,會嚴重影響我們的效率。

現在的CPU通常是多核狀態,在進行計算時,我們會有兩個以上 Registers,這樣的話,我們的PC就不需要頻繁的進行切換,我們的 ALU 處理計算的切換即可。

5.2 寄存器

在這裏插入圖片描述

5.3 Cache

我們通過上面的圖可以看出,我們的計算機為了獲取數據的方便性,增加了三級緩存,對於不同的緩存,獲取的時間的長短也是不一樣的

對於多核CPU來說,如下圖所示:
在這裏插入圖片描述

  • L1、L2存儲在不同的核中
  • L3存儲在同一個 CPU

5.3.1 局部性原理

簡單來說,我們的CPU在讀取數據時,將數據按快讀取,不單獨取一個字節,如下圖所示:
在這裏插入圖片描述
當前的CPU需要 X 這個目標值,步驟如下:

  • 第一步:去寄存器裏尋找,有沒有 X 這個字段
  • 第二步:去 L1、L2、L3 Cache 去尋找 X 這個字段
  • 第三步:去內存、磁盤等尋找 X 這個字段
  • 第四步:找到後,將以 X 開頭的 64個字節 形成一個塊
  • 第五步:在 L3、L2、L1 中分別存入這個數據,方便下次去拿緩存
  • 第六步:將 X 寫入寄存器,進行數據處理、

5.3.2 MESI Cache一致性協議

我們可以看到,對於上述兩個核的 L1、L2 的緩存行要保持一致,保持一致的協議被稱為:MESI 緩存一致性協議

CPU 每個 Cache line 標記四種狀態

  • M(已修改):該 Cache line 有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中。
  • E(獨占):該 Cache line 有效,數據和內存中的數據一致,數據只存在於本Cache中。
  • S(共享):該 Cache line 有效,數據和內存中的數據一致,數據存在於很多Cache中。
  • I(無效):Cache line 是無效的
    在這裏插入圖片描述

因特爾——緩存行

  • 緩存行越大,局部性空間效率越高,但讀取時間慢
  • 緩存行越小,局部性空間效率越低,但讀取時間快
  • 因特爾通過實驗規定,緩存行的大小為:64 字節

總線鎖(緩存行裝不下的情况下,就必須鎖總線)

緩存鎖實現之一,有些無法被緩存的數據或者跨越多個緩存行的數據,依然必須使用總線鎖

我們怎麼測試我們的猜想是正確的呢?

我們測試兩個程序

篇幅受限,源碼的話這裏暫時不展示了,有興趣的可以關注公眾號,回複:算法源碼

  • 第一個程序:數值的更改在同一個緩存行
    在這裏插入圖片描述
  • 第二個程序:數值的更改不在同一個緩存行
    在這裏插入圖片描述
    有上述程序驗證我們的猜想,兩個線程頻繁的更快緩存區中的緩存快,導致運行時間加長

5.3.3 緩存行對齊

對於有些特別敏感的數字,會存在線程高競爭的訪問,為了保證不發生偽共享,我們一般不要求緩存行對齊

簡單來說,我們不希望我們獲取 X 數字的同時,把 Y 也給獲取進來

在我們 JDK7disruptor 都采取long cache line padding

public long p1, p2, p3, p4, p5, p6, p7; // cache line padding
private volatile long cursor = INITIAL_CURSOR_VALUE;
public long p8, p9, p10, p11, p12, p13, p14; //cache line padding

這樣的話,當我們緩存行獲取時,就會把 cursor 前面的 long 或者 後面的 long 加載到緩存塊中,避免 cursor 的緩存行對齊

在我們的 JDK8 中,我們可以給該參數加入 @Contended(根據底層的CPU來進行設定,保證不會讓兩個參數共享一個緩存行),需要加上 -XX:-RestrictContended 生效

版权声明:本文为[愛敲代碼的小黃]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/01/202201070553380056.html