一. 計算機的五大組成部分
1. 控制器(Control):
2. 運算器(Datapath):
3. 存儲器(Memory):
4. 輸入(Input system):
5. 輸出(Output system):

輸入設備和輸出設備是如何工作的呢?
在電路板里,尤其是在cpu包含了大量的電路, 電路是根據電流的高低壓進行判斷, 根據電流的開/關不同, 會影響電信號的強度, 這樣就會導致兩種以結果, 一種是關閉了,還是沒關閉. 關閉了是0, 沒關閉是1. 所以為什么cpu只認0和1, 也是因為這個原因.
其實電路板, 我們都可以自制. 如何自制呢?
其實就是在一塊板子上, 鍍了一層銅膜, 然后設計電路, 在一類特殊類型的紙上設計, 設計好了以后, 把紙貼到鍍了銅的電路板上去. 紙上有我們設計好的電路, 然后把這個板子丟到化學反應池里, 然后沒有貼特殊紙的地方將會發生化學反應,把沒有貼到線路的銅膜擦掉
上面的模型是一個理論的抽象簡化模型,它的具體應用就是現代計算機當中的硬件結構設計:

現代計算機的構成主要有
- CPU
- 主板
- 內存條
- 硬盤
- 電源
一台電腦中, 上面這五個就是核心設備了,有了這5個,一台計算機就可以工作了.顯卡,鍵盤,鼠標,網卡,都可以沒有
cpu是非常重要的, 對於cpu我們需要重點了解兩個方面的內容
1. cpu是如何工作的.
2. cpu是如何和內存進行通訊的.
計算機主板上有很多的插槽,其中一部分是放cpu的,另一部分是放內存條的. 那么cpu和內存條怎么通信呢?
cpu和內存條都是插在主板上的,他們並不在一塊. 那么他們想要通信, 就要通過信道, 信道, 我們又可以叫做內存總線, 系統總線, IO總線. 他們是通過總線進行通訊的
二. CPU
2.1 CPU內部結構
cpu的內部構造很復雜, 主要有3個單元.
-
控制單元
- 運算單元
-
數據單元

最直接操作計算機的是什么語言? 是匯編語言
2.1.1 控制單元
2.1.2運算單元
2.1.3 存儲單元

2.2 CPU緩存結構
cpu對於計算機, 就像我們人的大腦.
常見的為三級緩存結構
-
L1 Cache,分為數據緩存和指令緩存,邏輯核獨占
-
L2 Cache,物理核獨占,邏輯核共享
-
L3 Cache,所有物理核共享
2.2.1 cpu為什么需要緩存呢?
根據摩爾定律, cpu的性能, 大概每過18個月, 性能就會翻一番. cpu從90年代到2020年, 經過了20多年的發展, 現在基本上進入了性能過剩的時代.
但是內存,經過了很多年的發展, 卻沒有什么摩爾定律, 我們知道內存從原來的DDR2 發展到 DDR3, 有發展到DDR4, 到現在的DDR5, 其實這個變化並沒有那么大
所以就造成了失衡的問題. 內存的速度遠遠跟不上CPU的速度了, 而每一次計算CPU都要去內存里拿數據, 這樣太耗費時間了. 所以, 為了解決這個問題, CPU增加了多級緩存. 增加多級緩存目的就是為了減少與內存的交互
2.2.2來看一下我們使用的計算機
看看這個計算機的基本信息, 此計算機有一個CPU. 那么這一個和我們之前說的1核,2核是一回事么? 不是一回事
.根據計算能力, 一台電腦可以安裝一個或多個CPU, CPU越多, 計算的速度相應的也會越快, 每個CPU可以有一核或者多核
問題: 現在有一個線程, 運行在4個cpu上, 和運行在有一個cpu, 但這個cpu有四個核, 哪一個更快呢?顯然收前者處理更快. 相應的性價比卻低很多.
如上圖, 還有內核和虛擬處理器, 一個是4, 一個是8. 這里虛擬處理器的含義是: 原來4核四線程, 那么每個容器可以執行一個線程. 這里虛擬化是指原來一個cpu內核只能跑一個線程, 現在可以跑兩個線程了.
2.2.3 CPU的三級緩存
還是看上圖, CPU有三級緩存, L1緩存 256kb, L2緩存 1.0MB L3緩存 8.0M L1 < L2 < L3.
L1離CPU是最近的, 離得越近速度越快. M < L3 < L2 < L1 < 寄存器. 數據拷貝的過程是, 從內存拷貝到L3, 在拷貝到L2, 在拷貝到L1, 再拷貝到寄存器.
CPU操作的數據只會去寄存器里面存或者取. 寄存器是每一個cpu獨有的. 這個寄存器只能被當前的cpu訪問到, 不能被其他cpu訪問
2.2.4 三級緩存和cpu有什么關系呢?
- L3是CPU內核共享的, 就是說被當前這個CPU的所有內核共享.
現在有兩個CPU, CPU2能不能訪問到CPU1的L3緩存呢?
答案是: 不能. 不能跨CPU訪問別人的緩存
- L1和L2是每個CPU上各內核獨享的.
- 一個內核只有一個L1和一個L2, 在圖中我們看到L1有兩個, 是因為L1有兩個功能, 一個是用來存儲指令, 另一個是用來存儲數據的. L1只有一個, 只是根據功能將其分為兩個部分.
- L1緩存分為兩種, 一種用來存儲指令, 另一種用來存儲數據.

現在大多數計算機的架構都是如上圖, 最底下是內存, 然后有一條總線連接cpu和內存條
2.2.5 我們能看到L3緩存有6M, 那么這6M沒有一個划分么? 就是一塊空間么?
不是的. 緩存也有一個最小的存儲單元, 這個最小的存儲單元叫做"緩存行" , 通常, 緩存行的大小是64byte, 市面上的cpu基本都是這么大. 那我有一個6M的緩存, 他有多少緩存行呢? 6 * 1024k * 1024b/64.緩存行是什么意思呢?它是CPU緩存中, 最小的空間存儲單位, 叫cacheline.

2.2.6 寄存器
2.2.7 緩存行的使用
現在cpu要開始計算了, 他需要獲取一個x參數, 首先, 他會去L1 一級緩存獲取, 一級緩存沒有, 再去二級緩存拿, 二級緩存沒有再去三級緩存拿, 三級緩存也沒有再去內存里找.
在內存找到了x=0, 然后將其拷貝到L3緩存中, 在拷貝到L2緩存, 在拷貝到L1緩存中. 然后在讀到寄存器里面去,供cpu使用
如果變量都拷貝到緩存里, 很快就會滿了, 最先滿的就是L1(他最小嘛) 然后, 他就有一個高淘汰的風險. 因為要騰出地方來給其他變量使用.
加入這個時候,L1中x=0變量被淘汰了, 當cpu需要計算的時候怎么辦呢? 沒關系, 因為寄存器里面已經有了x=0. 假如寄存器也淘汰了, 怎么辦呢? 他去L1也取不到, 那就去L2取, 如果L2也淘汰了,就去L3,總之, 他可以現在3級緩存中取數據, 不用立刻就去內存中取.
2.2.8 CPU讀取存儲器數據的過程
1、CPU要取寄存器X的值,只需要一步:直接讀取。
2、CPU要取L1 cache的某個值,需要1-3步(或者更多):把cache行鎖住,把某個數據拿來,解鎖,如果沒鎖住就慢了。
3、CPU要取L2 cache的某個值,先要到L1 cache里取,L1當中不存在,在L2里,L2開始加鎖,加鎖以后,把L2里的數據復制到L1,再執行讀L1的過程,上面的3步,再解鎖。
4、CPU取L3 cache的也是一樣,只不過先由L3復制到L2,從L2復制到L1,從L1到CPU。
5、CPU取內存則最復雜:通知內存控制器占用總線帶寬,通知內存加鎖,發起內存讀請求,等待回應,回應數據保存到L3(如果沒有就到L2),再從L3/2到L1,再從L1到CPU,之后解除總線鎖定。
2.2.9 CPU為何要有高速緩存
CPU在摩爾定律的指導下以每18個月翻一番的速度在發展,然而內存和硬盤的發展速度遠遠不及CPU。這就造成了高性能能的內存和硬盤價格及其昂貴。然而CPU的高度運算需要高速的數據。為了解決這個問題,CPU廠商在CPU中內置了少量的高速緩存以解決I\O速度和CPU運算速度之間的不匹配問題。
在CPU訪問存儲設備時,無論是存取數據抑或存取指令,都趨於聚集在一片連續的區域中,這就被稱為局部性原理。
-
時間局部性(Temporal Locality)
如果一個信息項正在被訪問,那么在近期它很可能還會被再次訪問。比如循環、遞歸、方法的反復調用等。
-
空間局部性(Spatial Locality):
如果一個存儲器的位置被引用,那么將來他附近的位置也會被引用。比如順序執行的代碼、連續創建的兩個對象、數組等。
2.2.9.1 什么是空間局部性原則呢?
我們都知道CPU和內存的交互是很慢的, 所以增加了3級緩存, 那好不容易交互一次, 費了很大勁就取了一個x=0, 是不是很浪費, 所以, CPU在和內存交互的時候, 不是只去x=0, 而是把x變量周邊的變量都會取回來. 比如y=1, z=2, n=3. CPU會一次性將這些數據都copy一份到3級緩存里去.
為什么這樣做呢?
如果一個變量被引用, 那么將來他附近的變量也會被引用, 因此, cpu會一次性將這些變量全部加載到cpu的緩存中.
如果一個存儲器的位置被引用,那么將來他附近的位置也會被引用。比如順序執行的代碼、連續創建的兩個對象、數組等。
舉個空間局部性原則例子:
package com.alibaba.nacos.test; /** * Description * <p> * </p> * DATE 2020/8/22. * * @author guolujie. */ public class TestSpace { private static Integer RUNS = 10; private static Integer DIMENSION_1 = 1024*1024; private static Integer DIMENSION_2 = 6; private static long[][] longs; public static void main(String[] args) { longs = new long[DIMENSION_1][DIMENSION_2]; for (int i = 0; i < DIMENSION_1; i ++) { for (int j = 0; j < DIMENSION_2; j ++) { longs[i][j] = 1l; } } System.out.println("構建二維數組完畢"); long sum = 0; long beginTime = System.currentTimeMillis(); for(int m = 0; m < RUNS; m ++) { for (int i = 0; i < DIMENSION_1; i++) { for (int j = 0; j < DIMENSION_2; j++) { sum += longs[i][j]; } } } System.out.println("sum1:" + sum); System.out.println("耗時:" + (System.currentTimeMillis() - beginTime)); sum = 0; beginTime = System.currentTimeMillis(); for (int m = 0; m < RUNS; m ++) { for (int i = 0; i < DIMENSION_2; i++) { for (int j = 0; j < DIMENSION_1; j++) { sum += longs[j][i]; } } } System.out.println("sum:" + sum); System.out.println("耗時:" + (System.currentTimeMillis() - beginTime)); } }
執行結果
構建二維數組完畢 sum1:62914560 耗時:179 sum:62914560 耗時:334
我們發現, 同樣是遍歷二維數組, 但是先遍歷哪一個, 得到的結論是不一樣的. 為什么會這樣呢?
這就是因為空間性原則.
數組一共有1024*1024行, 每一行有6個元素. 這6個元素是連接在一塊的. 如果我們先遍歷DIMESION_1, 那么, 就是每次循環進行6個數據相加, CPU在讀取數據的時候, 因為每一行的6個元素是挨着的, 所以, 根據空間局部性原則, 會一次從內存讀取這6個數據, 放入L3緩存中
而如果先遍歷EIMESION_2, 也就是每次循環進行1024*1024個數據相加, 但是, 這1024*1024個數據內存分配上是不連續的, 所以, 根據空間局部性原則, 每次只能讀取1個數據, 放入L3緩存中
那么對比兩次循環, CPU和內存一共交互了多少次呢?
第一個: 交互了1024*1024次
第二個: 交互了1024*1024*6次
所以,第二個耗時比第一個要多.
2.2.9.2 時間局部性原則
什么時間局部性原則呢? 下面舉個例子
X = 1 Y = 2 Z = X + Y a = 3 b = 4 c = a + b + X
通常來說, CPU在執行代碼的時候, 大多數情況下是按照時間的順序來執行的(也有不按時間順序執行的可能), 當CPU執行到X = 1, 並將其放入緩存, 后面執行完 Z = X + Y以后, 會刪除掉X么?
他不會立刻就刪除, 因為他認為, 后面還有可能會使用到變量X, 這就是時間局部性原則
如果一個信息項正在被訪問,那么在近期它很可能還會被再次訪問。比如循環、遞歸、方法的反復調用等。
2.2.10 帶有高速緩存的CPU執行計算的流程
-
1. 程序以及數據被加載到主內存
- 2. 指令和數據被加載到CPU的高速緩存
- 3. CPU執行指令,把結果寫到高速緩存
- 4. 高速緩存中的數據寫回主內存
3. 操作系統內存管理
我們經常聽到線程運行的時候, 有線程的上下文切換, 運行狀態的切換. 運行狀態有分為:內核態和用戶態.
通常我們的計算機是64位的, 內存大小在8G及以上.
32位的操作系統, 可用的內存空間是2的32次方. 尋址空間是0 - 2的32次方. 大約是4G.
從操作系統的層面, 對4G的內存做了一個分離, 分離成兩個部分, 一個是用戶空間, 一個是內核空間
3.1 執行空間保護
操作系統有用戶空間與內核空間兩個概念,目的也是為了做到程序運行安全隔離與穩定,以32位操作系統4G大小的內存空間為例
如下圖, 4G的空間, 其中有1G是內核空間, 有 3G是用戶空間.
- 用戶空間: 通常, 我們的JVM, 360瀏覽器, 各種APP都是運行在用戶空間的
- 內核空間: 操作系統是運行在內核空間的.
操作系統跑起來的時候, 就對內存進行了划分和隔離

3.1.1 用戶空間和內存空間, 與用戶態和內存態有什么區別呢? 他們之間為什么要做隔離呢?
我們將內存划分為內存空間和用戶空間, 很大一部分是為了保護用戶的操作系統. 什么意思呢?
就拿360瀏覽器來說, 一會報出一個病毒, 一會報出一個什么什么被劫持了, 很恐怖. 360這款軟件能訪問什么呢? 他只能訪問用戶空間, 而不能訪問內核空間.
想想一下, 如果360訪問了內核空間會怎么樣呢? 當我支付寶付款的時候, 其使用的內存被360劫持了, 系統時鍾不知道什么時候被360給修改了. 這是不是很恐怖的一件事?
所以, 在CPU里面, 做了一個安全等級的划分.
3.1.2 CPU運行安全等級
Inter將CPU划分為4個級別, 處在不同的安全級別, 能發出不同的安全指令.
CPU有4個運行級別,分別為:
- ring0
- ring1
- ring2
- ring3
其中ring0是安全級別最高的,他就是通常所講的內核態, CPU處在內核態下, 可以對操作系統執行任何權限級別的操作, 比如:修改內存, 刷磁盤.
通常, CPU都是運行在ring3級別下, 用戶態.
那么ring1和ring2有沒有用呢?
到目前為止, Linux與Windows只用到了兩種級別: ring0、ring3. 其中: 操作系統內部內部程序指令通常運行在ring0級別,操作系統以外的第三方程序運行在ring3級別,第三方程序如果要調用操作系統內部函數功能,由於運行安全級別不夠,必須切換CPU運行狀態,從ring3切換到ring0,然后執行系統函數,
說到這里, 我們來說說JVM創建線程
JVM創建線程, 歸根究底是操作系統去創建的, JVM要調用操作系統的庫, 這個庫叫做PThread, 而這個庫是由操作系統去管理的.
JVM要想調用操作系統的PThread庫, 不是輕松就調用的, 結合ring0和ring3我們來分析一下
我們知道JVM是運行在ring3, 用戶態. 而操作系統是運行在ring0, 內核態. 這時候JVM要想訪問操作系統的庫, 就要從ring3用戶態切換到ring0內核態.
當JVM從用戶態進入到內核態以后, JVM就原來的用戶態就不再有了, 而是轉移到了內核空間里面. 也就是原來的用戶空間的堆棧就都沒有了.
所以,我們的一個線程或者進程, 不只有一個堆棧, 而是有兩個. 一個在用戶態下, 一個在內核態下.
也就是說, 當線程創建出來以后, 都是運行在用戶空間的,一旦線程需要阻塞, 或者說要殺死, 那這個時候就要切換了, 從用戶態陷入到內核態去, 把原來的堆棧丟了
這就是CPU狀態狀態的切換
這回就明白為什么JVM創建線程,線程阻塞喚醒是重型操作了,因為CPU要切換運行狀態。
下面我大概梳理一下JVM創建線程CPU的工作過程
Linux為內核代碼和數據結構預留了幾個頁框,這些頁永遠不會被轉出到磁盤上。從
0x00000000 到 0xc0000000(PAGE_OFFSET) 的線性地址可由用戶代碼 和 內核代碼進行引用(即用戶空間)。從0xc0000000(PAGE_OFFSET)到 0xFFFFFFFFF的線性地址只能由內核代碼進行訪問(即內核空間)。內核代碼及其數據結構都必須位於這 1 GB的地址空間中,但是對於此地址空間而言,更大的消費者是物理地址的虛擬映射。
這意味着在 4 GB 的內存空間中,只有 3 GB 可以用於用戶應用程序。進程與線程只能運行在用戶方式(usermode)或內核方式(kernelmode)下。用戶程序運行在用戶方式下,而系統調用運行在內核方式下。在這兩種方式下所用的堆棧不一樣:用戶方式下用的是一般的堆棧(用戶空間的堆棧),而內核方式下用的是固定大小的堆棧(內核空間的對戰,一般為一個內存頁的大小),即每個進程與線程其實有兩個堆棧,分別運行與用戶態與內核態。
3.2 線程模型
內核線程模型

用戶線程模型
用戶線程(ULT):用戶程序實現,不依賴操作系統核心,應用提供創建、同步、調度和管理線程的函數來控制用戶線程。不需要用戶態/內核態切換,速度快。內核對ULT無感知,線程阻塞則進程(包括它的所有線程)阻塞。
思考一下,jvm是采用的哪一種線程模型?
java線程的創建, 肯定會被操作系統感知到. 下面我們來操作模擬一下, 創建200個線程,啟動程序, 線程立刻增加200.
4. 線程的上下文切換
4.1 進程與線程
什么是進程?
現代操作系統在運行一個程序時,會為其創建一個進程;例如,啟動一個Java程序,操作系統就會創建一個Java進程。進程是OS(操作系統)資源分配的最小單位。什么是線程?
線程是OS(操作系統)調度CPU的最小單元,也叫輕量級進程(Light Weight Process),在一個進程里可以創建多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,並且能夠訪問共享的內存變量。CPU在這些線程上高速切換,讓使用者感覺到這些線程在同時執行,即並發的概念,相似的概念還有並行!
4.2 線程上下文切換過程:

cpu是采用時間片的方式, 在線程執行的時候, 他會給大家分配時間周期, 比如: 現在有兩個線程T1和T2, 在調度的時候先分配時間, T1是是50ns, T是10ns. cpu先執行T1, T1的時鍾周期到了以后, 就會把T1運行的中間狀態保存起來, 再去執行T2, T2的時鍾周期運行完了以后, 在把T2的時間狀態保存起來, 然后再去執行T1. 如此循環往復.
cpu是采用時間片的方式, 在輪詢的時候, 上一個線程還沒有執行完, 就會把中間結果保存起來, 在執行下一個線程. 這就是線程的上下文切換.
數據保存在TSS(Task State Segament)程序任務狀態段. 專門保存程序上下文的任務狀態區間.
虛擬機指令集架構
虛擬機指令集架構主要分兩種:
1、棧指令集架構
2、寄存器指令集架構
關於指令集架構的wiki詳細說明:
https://zh.wikipedia.org/wiki/%E6%8C%87%E4%BB%A4%E9%9B%86%E6%9E%B6%E6%A7%8B
棧指令集架構
1. 設計和實現更簡單,適用於資源受限的系統;
2. 避開了寄存器的分配難題:使用零地址指令方式分配;
3. 指令流中的指令大部分是零地址指令,其執行過程依賴與操作棧,指令集更小,編譯器容易實現;
4. 不需要硬件支持,可移植性更好,更好實現跨平台。
寄存器指令集架構
1. 典型的應用是x86的二進制指令集:比如傳統的PC以及Android的Davlik虛擬機。
2. 指令集架構則完全依賴硬件,可移植性差。
3. 性能優秀和執行更高效。
4. 花費更少的指令去完成一項操作。
5. 在大部分情況下,基於寄存器架構的指令集往往都以一地址指令、二地址指令和三地址指令為主,而基於棧式架構的指令集卻是以零地址指令為主。
比如:
A = 1; B = 2; C = A + B;
如果是寄存器指令集架構: 直接將A和B丟給cpu, 告訴cpu去執行相加, cpu就做了加法操作, 返回返回, 這個操作簡單高效, 這就是寄存器指令集架構的運算方式
棧指令集架構: 同樣是上面的操作, 棧指令集架構, 不會直接講A和B丟給CPU, 而是先把A push到內存區域里去, 每個內存都有個線程棧, 也就是先把A push到線程棧里面, 每個線程棧還有局部變量表, 把A=0從線程棧里彈出來, 放入到局部變量表里. 接着, 在棧里面在壓入B = 1, 然后再把b=1彈出放入局部變量表里, 最后面在執行 0 + 1 這個操作
上面每次入棧和出棧是誰干的? 是cpu干的, 在cpu來看, 從線程棧到局部變量表就是一個數據的拷貝過程.