自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

程序設(shè)計(jì)的5個(gè)底層邏輯,決定你能走多快

開發(fā) 前端
肉眼看計(jì)算機(jī)是由CPU、內(nèi)存、顯示器這些硬件設(shè)備組成,但大部分人從事的是軟件開發(fā)工作。

[[283223]]

肉眼看計(jì)算機(jī)是由CPU、內(nèi)存、顯示器這些硬件設(shè)備組成,但大部分人從事的是軟件開發(fā)工作。計(jì)算機(jī)底層原理就是連通硬件和軟件的橋梁,理解計(jì)算機(jī)底層原理才能在程序設(shè)計(jì)這條路上越走越快,越走越輕松。從操作系統(tǒng)層面去理解高級編程語言的執(zhí)行過程,會(huì)發(fā)現(xiàn)好多軟件設(shè)計(jì)都是同一種套路,很多語言特性都依賴于底層機(jī)制,今天董鵬為你一一揭秘。

結(jié)合 CPU 理解一行 Java 代碼是怎么執(zhí)行的

根據(jù)馮·諾依曼思想,計(jì)算機(jī)采用二進(jìn)制作為數(shù)制基礎(chǔ),必須包含:運(yùn)算器、控制器、存儲(chǔ)設(shè)備,以及輸入輸出設(shè)備,如下圖所示。

 

圖片來源:http://m.elecfans.com/article/625138.html

我們先來分析 CPU 的工作原理,現(xiàn)代 CPU 芯片中大都集成了,控制單元,運(yùn)算單元,存儲(chǔ)單元。控制單元是 CPU 的控制中心, CPU 需要通過它才知道下一步做什么,也就是執(zhí)行什么指令,控制單元又包含:指令寄存器(IR ),指令譯碼器( ID )和操作控制器( OC )。

當(dāng)程序被加載進(jìn)內(nèi)存后,指令就在內(nèi)存中了,這個(gè)時(shí)候說的內(nèi)存是獨(dú)立于 CPU 外的主存設(shè)備,也就是 PC 機(jī)中的內(nèi)存條,指令指針寄存器IP 指向內(nèi)存中下一條待執(zhí)行指令的地址,控制單元根據(jù) IP寄存器的指向,將主存中的指令裝載到指令寄存器。

這個(gè)指令寄存器也是一個(gè)存儲(chǔ)設(shè)備,不過他集成在 CPU 內(nèi)部,指令從主存到達(dá) CPU 后只是一串 010101 的二進(jìn)制串,還需要通過譯碼器解碼,分析出操作碼是什么,操作數(shù)在哪,之后就是具體的運(yùn)算單元進(jìn)行算術(shù)運(yùn)算(加減乘除),邏輯運(yùn)算(比較,位移)。而 CPU 指令執(zhí)行過程大致為:取址(去主存獲取指令放到寄存器),譯碼(從主存獲取操作數(shù)放入高速緩存 L1 ),執(zhí)行(運(yùn)算)。

 

這里解釋下上圖中 CPU 內(nèi)部集成的存儲(chǔ)單元 SRAM ,正好和主存中的 DRAM 對應(yīng), RAM 是隨機(jī)訪問內(nèi)存,就是給一個(gè)地址就能訪問到數(shù)據(jù),而磁盤這種存儲(chǔ)媒介必須順序訪問,而 RAM 又分為動(dòng)態(tài)和靜態(tài)兩種,靜態(tài) RAM 由于集成度較低,一般容量小,速度快,而動(dòng)態(tài) RAM 集成度較高,主要通過給電容充電和放電實(shí)現(xiàn),速度沒有靜態(tài) RAM 快,所以一般將動(dòng)態(tài) RAM 做為主存,而靜態(tài) RAM 作為 CPU 和主存之間的高速緩存 (cache),用來屏蔽 CPU 和主存速度上的差異,也就是我們經(jīng)??吹降?L1 , L2 緩存。每一級別緩存速度變低,容量變大。

下圖展示了存儲(chǔ)器的層次化架構(gòu),以及 CPU 訪問主存的過程,這里有兩個(gè)知識點(diǎn),一個(gè)是多級緩存之間為保證數(shù)據(jù)的一致性,而推出的緩存一致性協(xié)議,具體可以參考這篇文章,另外一個(gè)知識點(diǎn)是, cache 和主存的映射,首先要明確的是 cahce 緩存的單位是緩存行,對應(yīng)主存中的一個(gè)內(nèi)存塊,并不是一個(gè)變量,這個(gè)主要是因?yàn)?CPU 訪問的空間局限性:被訪問的某個(gè)存儲(chǔ)單元,在一個(gè)較短時(shí)間內(nèi),很有可能再次被訪問到,以及空間局限性:被訪問的某個(gè)存儲(chǔ)單元,在較短時(shí)間內(nèi),他的相鄰存儲(chǔ)單元也會(huì)被訪問到。

而映射方式有很多種,類似于 cache 行號 = 主存塊號 mod cache總行數(shù) ,這樣每次獲取到一個(gè)主存地址,根據(jù)這個(gè)地址計(jì)算出在主存中的塊號就可以計(jì)算出在 cache 中的行號。

下面我們接著聊 CPU 的指令執(zhí)行。取址、譯碼、執(zhí)行,這是一個(gè)指令的執(zhí)行過程,所有指令都會(huì)嚴(yán)格按照這個(gè)順序執(zhí)行。但是多個(gè)指令之間其實(shí)是可以并行的,對于單核 CPU 來說,同一時(shí)刻只能有一條指令能夠占有執(zhí)行單元運(yùn)行。這里說的執(zhí)行是 CPU 指令處理 (取指,譯碼,執(zhí)行) 三步驟中的第三步,也就是運(yùn)算單元的計(jì)算任務(wù)。

所以為了提升 CPU 的指令處理速度,所以需要保證運(yùn)算單元在執(zhí)行前的準(zhǔn)備工作都完成,這樣運(yùn)算單元就可以一直處于運(yùn)算中,而剛剛的串行流程中,取指,解碼的時(shí)候運(yùn)算單元是空閑的,而且取指和解碼如果沒有命中高速緩存還需要從主存取,而主存的速度和 CPU 不在一個(gè)級別上,所以指令流水線 可以大大提高 CPU 的處理速度,下圖是一個(gè)3級流水線的示例圖,而現(xiàn)在的奔騰 CPU 都是32級流水線,具體做法就是將上面三個(gè)流程拆分的更細(xì)。

 

除了指令流水線, CPU 還有分支預(yù)測,亂序執(zhí)行等優(yōu)化速度的手段。好了,我們回到正題,一行 Java 代碼是怎么執(zhí)行的?

一行代碼能夠執(zhí)行,必須要有可以執(zhí)行的上下文環(huán)境,包括:指令寄存器、數(shù)據(jù)寄存器、棧空間等內(nèi)存資源,然后這行代碼必須作為一個(gè)執(zhí)行流能夠被操作系統(tǒng)的任務(wù)調(diào)度器識別,并給他分配 CPU 資源,當(dāng)然這行代碼所代表的指令必須是 CPU 可以解碼識別的,所以一行 Java 代碼必須被解釋成對應(yīng)的 CPU 指令才能執(zhí)行。下面我們看下System.out.println("Hello world")這行代碼的轉(zhuǎn)譯過程。

Java 是一門高級語言,這類語言不能直接運(yùn)行在硬件上,必須運(yùn)行在能夠識別 Java 語言特性的虛擬機(jī)上,而 Java 代碼必須通過 Java 編譯器將其轉(zhuǎn)換成虛擬機(jī)所能識別的指令序列,也稱為 Java 字節(jié)碼,之所以稱為字節(jié)碼是因?yàn)?Java 字節(jié)碼的操作指令(OpCode)被固定為一個(gè)字節(jié),以下為 System.out.println("Hello world") 編譯后的字節(jié)碼:

  1. 0x00:  b2 00 02         getstatic  Java .lang.System.out 
  2. 0x03:  12 03            ldc "Hello, World!" 
  3. 0x05:  b6 00 04         invokevirtual  Java .io.PrintStream.println 
  4. 0x08:  b1               return 

最左列是偏移;中間列是給虛擬機(jī)讀的字節(jié)碼;最右列是高級語言的代碼,下面是通過匯編語言轉(zhuǎn)換成的機(jī)器指令,中間是機(jī)器碼,第三列為對應(yīng)的機(jī)器指令,最后一列是對應(yīng)的匯編代碼:

  1. 0x00:  55                    push   rbp 
  2. 0x01:  48 89 e5              mov    rbp,rsp 
  3. 0x04:  48 83 ec 10           sub    rsp,0x10 
  4. 0x08:  48 8d 3d 3b 00 00 00  lea    rdi,[rip+0x3b] 
  5.                                     ; 加載 "Hello, World!\n" 
  6. 0x0f:  c7 45 fc 00 00 00 00  mov    DWORD PTR [rbp-0x4],0x0 
  7. 0x16:  b0 00                 mov    al,0x0 
  8. 0x18:  e8 0d 00 00 00        call   0x12 
  9.                                     ; 調(diào)用 printf 方法 
  10. 0x1d:  31 c9                 xor    ecx,ecx 
  11. 0x1f:  89 45 f8              mov    DWORD PTR [rbp-0x8],eax 
  12. 0x22:  89 c8                 mov    eax,ecx 
  13. 0x24:  48 83 c4 10           add    rsp,0x10 
  14. 0x28:  5d                    pop    rbp 
  15. 0x29:  c3                    ret 

JVM 通過類加載器加載 class 文件里的字節(jié)碼后,會(huì)通過解釋器解釋成匯編指令,最終再轉(zhuǎn)譯成 CPU 可以識別的機(jī)器指令,解釋器是軟件來實(shí)現(xiàn)的,主要是為了實(shí)現(xiàn)同一份 Java 字節(jié)碼可以在不同的硬件平臺(tái)上運(yùn)行,而將匯編指令轉(zhuǎn)換成機(jī)器指令由硬件直接實(shí)現(xiàn),這一步速度是很快的,當(dāng)然 JVM 為了提高運(yùn)行效率也可以將某些熱點(diǎn)代碼(一個(gè)方法內(nèi)的代碼)一次全部編譯成機(jī)器指令后然后在執(zhí)行,也就是和解釋執(zhí)行對應(yīng)的即時(shí)編譯(JIT), JVM 啟動(dòng)的時(shí)候可以通過 -Xint 和 -Xcomp 來控制執(zhí)行模式。

從軟件層面上, class 文件被加載進(jìn)虛擬機(jī)后,類信息會(huì)存放在方法區(qū),在實(shí)際運(yùn)行的時(shí)候會(huì)執(zhí)行方法區(qū)中的代碼,在 JVM 中所有的線程共享堆內(nèi)存和方法區(qū),而每個(gè)線程有自己獨(dú)立的 Java 方法棧,本地方法棧(面向 native 方法),PC寄存器(存放線程執(zhí)行位置),當(dāng)調(diào)用一個(gè)方法的時(shí)候, Java 虛擬機(jī)會(huì)在當(dāng)前線程對應(yīng)的方法棧中壓入一個(gè)棧幀,用來存放 Java 字節(jié)碼操作數(shù)以及局部變量,這個(gè)方法執(zhí)行完會(huì)彈出棧幀,一個(gè)線程會(huì)連續(xù)執(zhí)行多個(gè)方法,對應(yīng)不同的棧幀的壓入和彈出,壓入棧幀后就是 JVM 解釋執(zhí)行的過程了。

 

中斷

剛剛說到, CPU 只要一上電就像一個(gè)永動(dòng)機(jī), 不停的取指令,運(yùn)算,周而復(fù)始,而中斷便是操作系統(tǒng)的靈魂,故名思議,中斷就是打斷 CPU 的執(zhí)行過程,轉(zhuǎn)而去做點(diǎn)別的。

例如系統(tǒng)執(zhí)行期間發(fā)生了致命錯(cuò)誤,需要結(jié)束執(zhí)行,例如用戶程序調(diào)用了一個(gè)系統(tǒng)調(diào)用的方法,例如mmp等,就會(huì)通過中斷讓 CPU 切換上下文,轉(zhuǎn)到內(nèi)核空間,例如一個(gè)等待用戶輸入的程序正在阻塞,而當(dāng)用戶通過鍵盤完成輸入,內(nèi)核數(shù)據(jù)已經(jīng)準(zhǔn)備好后,就會(huì)發(fā)一個(gè)中斷信號,喚醒用戶程序把數(shù)據(jù)從內(nèi)核取走,不然內(nèi)核可能會(huì)數(shù)據(jù)溢出,當(dāng)磁盤報(bào)了一個(gè)致命異常,也會(huì)通過中斷通知 CPU ,定時(shí)器完成時(shí)鐘滴答也會(huì)發(fā)時(shí)鐘中斷通知 CPU 。

中斷的種類,我們這里就不做細(xì)分了,中斷有點(diǎn)類似于我們經(jīng)常說的事件驅(qū)動(dòng)編程,而這個(gè)事件通知機(jī)制是怎么實(shí)現(xiàn)的呢,硬件中斷的實(shí)現(xiàn)通過一個(gè)導(dǎo)線和 CPU 相連來傳輸中斷信號,軟件上會(huì)有特定的指令,例如執(zhí)行系統(tǒng)調(diào)用創(chuàng)建線程的指令,而 CPU 每執(zhí)行完一個(gè)指令,就會(huì)檢查中斷寄存器中是否有中斷,如果有就取出然后執(zhí)行該中斷對應(yīng)的處理程序。

陷入內(nèi)核 : 我們在設(shè)計(jì)軟件的時(shí)候,會(huì)考慮程序上下文切換的頻率,頻率太高肯定會(huì)影響程序執(zhí)行性能,而陷入內(nèi)核是針對 CPU 而言的, CPU 的執(zhí)行從用戶態(tài)轉(zhuǎn)向內(nèi)核態(tài),以前是用戶程序在使用 CPU ,現(xiàn)在是內(nèi)核程序在使用 CPU ,這種切換是通過系統(tǒng)調(diào)用產(chǎn)生的。

系統(tǒng)調(diào)用是執(zhí)行操作系統(tǒng)底層的程序,Linux的設(shè)計(jì)者,為了保護(hù)操作系統(tǒng),將進(jìn)程的執(zhí)行狀態(tài)用內(nèi)核態(tài)和用戶態(tài)分開,同一個(gè)進(jìn)程中,內(nèi)核和用戶共享同一個(gè)地址空間,一般 4G 的虛擬地址,其中 1G 給內(nèi)核態(tài), 3G 給用戶態(tài)。在程序設(shè)計(jì)的時(shí)候我們要盡量減少用戶態(tài)到內(nèi)核態(tài)的切換,例如創(chuàng)建線程是一個(gè)系統(tǒng)調(diào)用,所以我們有了線程池的實(shí)現(xiàn)。 

從 Linux 內(nèi)存管理角度理解 JVM 內(nèi)存模型

進(jìn)程上下文

我們可以將程序理解為一段可執(zhí)行的指令集合,而這個(gè)程序啟動(dòng)后,操作系統(tǒng)就會(huì)為他分配 CPU ,內(nèi)存等資源,而這個(gè)正在運(yùn)行的程序就是我們說的進(jìn)程,進(jìn)程是操作系統(tǒng)對處理器中運(yùn)行的程序的一種抽象。

而為進(jìn)程分配的內(nèi)存以及 CPU 資源就是這個(gè)進(jìn)程的上下文,保存了當(dāng)前執(zhí)行的指令,以及變量值,而 JVM 啟動(dòng)后也是linux上的一個(gè)普通進(jìn)程,進(jìn)程的物理實(shí)體和支持進(jìn)程運(yùn)行的環(huán)境合稱為上下文,而上下文切換就是將當(dāng)前正在運(yùn)行的進(jìn)程換下,換一個(gè)新的進(jìn)程到處理器運(yùn)行,以此來讓多個(gè)進(jìn)程并發(fā)的執(zhí)行,上下文切換可能來自操作系統(tǒng)調(diào)度,也有可能來自程序內(nèi)部,例如讀取IO的時(shí)候,會(huì)讓用戶代碼和操作系統(tǒng)代碼之間進(jìn)行切換。

 

虛擬存儲(chǔ)

當(dāng)我們同時(shí)啟動(dòng)多個(gè) JVM 執(zhí)行:System.out.println(new Object()); 將會(huì)打印這個(gè)對象的 hashcode ,hashcode 默認(rèn)為內(nèi)存地址,最后發(fā)現(xiàn)他們打印的都是 Java .lang.Object@4fca772d ,也就是多個(gè)進(jìn)程返回的內(nèi)存地址竟然是一樣的。

通過上面的例子我們可以證明,linux中每個(gè)進(jìn)程有單獨(dú)的地址空間,在此之前,我們先了解下 CPU 是如何訪問內(nèi)存的?

假設(shè)我們現(xiàn)在還沒有虛擬地址,只有物理地址,編譯器在編譯程序的時(shí)候,需要將高級語言轉(zhuǎn)換成機(jī)器指令,那么 CPU 訪問內(nèi)存的時(shí)候必須指定一個(gè)地址,這個(gè)地址如果是一個(gè)絕對的物理地址,那么程序就必須放在內(nèi)存中的一個(gè)固定的地方,而且這個(gè)地址需要在編譯的時(shí)候就要確認(rèn),大家應(yīng)該想到這樣有多坑了吧。

如果我要同時(shí)運(yùn)行兩個(gè) office word 程序,那么他們將操作同一塊內(nèi)存,那就亂套了,偉大的計(jì)算機(jī)前輩設(shè)計(jì)出,讓 CPU 采用 段基址 + 段內(nèi)偏移地址 的方式訪問內(nèi)存,其中段基地址在程序啟動(dòng)的時(shí)候確認(rèn),盡管這個(gè)段基地址還是絕對的物理地址,但終究可以同時(shí)運(yùn)行多個(gè)程序了, CPU 采用這種方式訪問內(nèi)存,就需要段基址寄存器和段內(nèi)偏移地址寄存器來存儲(chǔ)地址,最終將兩個(gè)地址相加送上地址總線。

而內(nèi)存分段,相當(dāng)于每個(gè)進(jìn)程都會(huì)分配一個(gè)內(nèi)存段,而且這個(gè)內(nèi)存段需要是一塊連續(xù)的空間,主存里維護(hù)著多個(gè)內(nèi)存段,當(dāng)某個(gè)進(jìn)程需要更多內(nèi)存,并且超出物理內(nèi)存的時(shí)候,就需要將某個(gè)不常用的內(nèi)存段換到硬盤上,等有充足內(nèi)存的時(shí)候在從硬盤加載進(jìn)來,也就是 swap 。每次交換都需要操作整個(gè)段的數(shù)據(jù)。

首先連續(xù)的地址空間是很寶貴的,例如一個(gè) 50M 的內(nèi)存,在內(nèi)存段之間有空隙的情況下,將無法支持 5 個(gè)需要 10M 內(nèi)存才能運(yùn)行的程序,如何才能讓段內(nèi)地址不連續(xù)呢? 答案是內(nèi)存分頁。 

在保護(hù)模式下,每一個(gè)進(jìn)程都有自己獨(dú)立的地址空間,所以段基地址是固定的,只需要給出段內(nèi)偏移地址就可以了,而這個(gè)偏移地址稱為線性地址,線性地址是連續(xù)的,而內(nèi)存分頁將連續(xù)的線性地址和和分頁后的物理地址相關(guān)聯(lián),這樣邏輯上的連續(xù)線性地址可以對應(yīng)不連續(xù)的物理地址。

物理地址空間可以被多個(gè)進(jìn)程共享,而這個(gè)映射關(guān)系將通過頁表( page table)進(jìn)行維護(hù)。 標(biāo)準(zhǔn)頁的尺寸一般為 4KB ,分頁后,物理內(nèi)存被分成若干個(gè) 4KB 的數(shù)據(jù)頁,進(jìn)程申請內(nèi)存的時(shí)候,可以映射為多個(gè) 4KB 大小的物理內(nèi)存,而應(yīng)用程序讀取數(shù)據(jù)的時(shí)候會(huì)以頁為最小單位,當(dāng)需要和硬盤發(fā)生交換的時(shí)候也是以頁為單位。

現(xiàn)代計(jì)算機(jī)多采用虛擬存儲(chǔ)技術(shù),虛擬存儲(chǔ)讓每個(gè)進(jìn)程以為自己獨(dú)占整個(gè)內(nèi)存空間,其實(shí)這個(gè)虛擬空間是主存和磁盤的抽象,這樣的好處是,每個(gè)進(jìn)程擁有一致的虛擬地址空間,簡化了內(nèi)存管理,進(jìn)程不需要和其他進(jìn)程競爭內(nèi)存空間。

因?yàn)樗仟?dú)占的,也保護(hù)了各自進(jìn)程不被其他進(jìn)程破壞,另外,他把主存看成磁盤的一個(gè)緩存,主存中僅保存活動(dòng)的程序段和數(shù)據(jù)段,當(dāng)主存中不存在數(shù)據(jù)的時(shí)候發(fā)生缺頁中斷,然后從磁盤加載進(jìn)來,當(dāng)物理內(nèi)存不足的時(shí)候會(huì)發(fā)生 swap 到磁盤。頁表保存了虛擬地址和物理地址的映射,頁表是一個(gè)數(shù)組,每個(gè)元素為一個(gè)頁的映射關(guān)系,這個(gè)映射關(guān)系可能是和主存地址,也可能和磁盤,頁表存儲(chǔ)在主存,我們將存儲(chǔ)在高速緩沖區(qū) cache 中的頁表稱為快表 TLAB 。

 

  • 裝入位 表示對于頁是否在主存,如果地址頁每頁表示,數(shù)據(jù)還在磁盤
  • 存放位置 建立虛擬頁和物理頁的映射,用于地址轉(zhuǎn)換,如果為null表示是一個(gè)未分配頁
  • 修改位 用來存儲(chǔ)數(shù)據(jù)是否修改過
  • 權(quán)限位 用來控制是否有讀寫權(quán)限
  • 禁止緩存位 主要用來保證 cache 主存 磁盤的數(shù)據(jù)一致性

內(nèi)存映射

正常情況下,我們讀取文件的流程為,先通過系統(tǒng)調(diào)用從磁盤讀取數(shù)據(jù),存入操作系統(tǒng)的內(nèi)核緩沖區(qū),然后在從內(nèi)核緩沖區(qū)拷貝到用戶空間,而內(nèi)存映射,是將磁盤文件直接映射到用戶的虛擬存儲(chǔ)空間中,通過頁表維護(hù)虛擬地址到磁盤的映射,通過內(nèi)存映射的方式讀取文件的好處有,因?yàn)闇p少了從內(nèi)核緩沖區(qū)到用戶空間的拷貝,直接從磁盤讀取數(shù)據(jù)到內(nèi)存,減少了系統(tǒng)調(diào)用的開銷,對用戶而言,仿佛直接操作的磁盤上的文件,另外由于使用了虛擬存儲(chǔ),所以不需要連續(xù)的主存空間來存儲(chǔ)數(shù)據(jù)。

 

在 Java 中,我們使用 MappedByteBuffer 來實(shí)現(xiàn)內(nèi)存映射,這是一個(gè)堆外內(nèi)存,在映射完之后,并沒有立即占有物理內(nèi)存,而是訪問數(shù)據(jù)頁的時(shí)候,先查頁表,發(fā)現(xiàn)還沒加載,發(fā)起缺頁異常,然后在從磁盤將數(shù)據(jù)加載進(jìn)內(nèi)存,所以一些對實(shí)時(shí)性要求很高的中間件,例如rocketmq,消息存儲(chǔ)在一個(gè)大小為1G的文件中,為了加快讀寫速度,會(huì)將這個(gè)文件映射到內(nèi)存后,在每個(gè)頁寫一比特?cái)?shù)據(jù),這樣就可以把整個(gè)1G文件都加載進(jìn)內(nèi)存,在實(shí)際讀寫的時(shí)候就不會(huì)發(fā)生缺頁了,這個(gè)在rocketmq內(nèi)部叫做文件預(yù)熱。

下面我們貼一段 rocketmq 消息存儲(chǔ)模塊的代碼,位于 MappedFile 類中,這個(gè)類是 rocketMq 消息存儲(chǔ)的核心類感興趣的可以自行研究,下面兩個(gè)方法一個(gè)是創(chuàng)建文件映射,一個(gè)是預(yù)熱文件,每預(yù)熱 1000 個(gè)數(shù)據(jù)頁,就讓出 CPU 權(quán)限。

  1. private void init(final String fileName, final int fileSize) throws IOException { 
  2.         this.fileName = fileName; 
  3.         this.fileSize = fileSize; 
  4.         this.file = new File(fileName); 
  5.         this.fileFromOffset = Long.parseLong(this.file.getName()); 
  6.         boolean ok = false
  7.  
  8.         ensureDirOK(this.file.getParent()); 
  9.  
  10.         try { 
  11.             this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel(); 
  12.             this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize); 
  13.             TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize); 
  14.             TOTAL_MAPPED_FILES.incrementAndGet(); 
  15.             ok = true
  16.         } catch (FileNotFoundException e) { 
  17.             log.error("create file channel " + this.fileName + " Failed. ", e); 
  18.             throw e; 
  19.         } catch (IOException e) { 
  20.             log.error("map file " + this.fileName + " Failed. ", e); 
  21.             throw e; 
  22.         } finally { 
  23.             if (!ok && this.fileChannel != null) { 
  24.                 this.fileChannel.close(); 
  25.             } 
  26.         } 
  27.     } 

JVM 中對象的內(nèi)存布局

在linux中只要知道一個(gè)變量的起始地址就可以讀出這個(gè)變量的值,因?yàn)閺倪@個(gè)起始地址起前8位記錄了變量的大小,也就是可以定位到結(jié)束地址,在 Java 中我們可以通過 Field.get(object) 的方式獲取變量的值,也就是反射,最終是通過 UnSafe 類來實(shí)現(xiàn)的。我們可以分析下具體代碼。

  1. Field 對象的 getInt方法  先安全檢查 ,然后調(diào)用 FieldAccessor 
  2.     @CallerSensitive 
  3.     public int getInt(Object obj) 
  4.         throws IllegalArgumentException, IllegalAccessException 
  5.     { 
  6.         if (!override) { 
  7.             if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { 
  8.                 Class<?> caller = Reflection.getCallerClass(); 
  9.                 checkAccess(caller, clazz, obj, modifiers); 
  10.             } 
  11.         } 
  12.         return getFieldAccessor(obj).getInt(obj); 
  13.     } 
  14.  
  15.  
  16.  獲取field在所在對象中的地址的偏移量 fieldoffset 
  17.     UnsafeFieldAccessorImpl(Field var1) { 
  18.             this.field = var1; 
  19.             if(Modifier.isStatic(var1.getModifiers())) { 
  20.                 this.fieldOffset = unsafe.staticFieldOffset(var1); 
  21.             } else { 
  22.                 this.fieldOffset = unsafe.objectFieldOffset(var1); 
  23.             } 
  24.  
  25.             this.isFinal = Modifier.isFinal(var1.getModifiers()); 
  26.      } 
  27.  
  28.  
  29.  UnsafeStaticIntegerFieldAccessorImpl 調(diào)用unsafe中的方法 
  30.      public int getInt(Object var1) throws IllegalArgumentException { 
  31.           return unsafe.getInt(this.base, this.fieldOffset); 
  32.      } 

通過上面的代碼我們可以通過屬性相對對象起始地址的偏移量,來讀取和寫入屬性的值,這也是 Java 反射的原理,這種模式在jdk中很多場景都有用到,例如LockSupport.park中設(shè)置阻塞對象。 那么屬性的偏移量具體根據(jù)什么規(guī)則來確定的呢? 下面我們借此機(jī)會(huì)分析下 Java 對象的內(nèi)存布局。

在 Java 虛擬機(jī)中,每個(gè) Java 對象都有一個(gè)對象頭 (object header) ,由標(biāo)記字段和類型指針構(gòu)成,標(biāo)記字段用來存儲(chǔ)對象的哈希碼, GC 信息, 持有的鎖信息,而類型指針指向該對象的類 Class ,在 64 位操作系統(tǒng)中,標(biāo)記字段占有 64 位,而類型指針也占 64 位,也就是說一個(gè) Java 對象在什么屬性都沒有的情況下要占有 16 字節(jié)的空間,當(dāng)前 JVM 中默認(rèn)開啟了壓縮指針,這樣類型指針可以只占 32 位,所以對象頭占 12 字節(jié), 壓縮指針可以作用于對象頭,以及引用類型的字段。

JVM 為了內(nèi)存對齊,會(huì)對字段進(jìn)行重排序,這里的對齊主要指 Java 虛擬機(jī)堆中的對象的起始地址為 8 的倍數(shù),如果一個(gè)對象用不到 8N 個(gè)字節(jié),那么剩下的就會(huì)被填充,另外子類繼承的屬性的偏移量和父類一致,以 Long 為例,他只有一個(gè)非 static 屬性 value ,而盡管對象頭只占有 12 字節(jié),而屬性 value 的偏移量只能是 16, 其中 4 字節(jié)只能浪費(fèi)掉,所以字段重排就是為了避免內(nèi)存浪費(fèi), 所以我們很難在 Java 字節(jié)碼被加載之前分析出這個(gè) Java 對象占有的實(shí)際空間有多大,我們只能通過遞歸父類的所有屬性來預(yù)估對象大小,而真實(shí)占用的大小可以通過 Java agent 中的 Instrumentation獲取。

當(dāng)然內(nèi)存對齊另外一個(gè)原因是為了讓字段只出現(xiàn)在同一個(gè) CPU 的緩存行中,如果字段不對齊,就有可能出現(xiàn)一個(gè)字段的一部分在緩存行 1 中,而剩下的一半在 緩存行 2 中,這樣該字段的讀取需要替換兩個(gè)緩存行,而字段的寫入會(huì)導(dǎo)致兩個(gè)緩存行上緩存的其他數(shù)據(jù)都無效,這樣會(huì)影響程序性能。

通過內(nèi)存對齊可以避免一個(gè)字段同時(shí)存在兩個(gè)緩存行里的情況,但還是無法完全規(guī)避緩存?zhèn)喂蚕淼膯栴},也就是一個(gè)緩存行中存了多個(gè)變量,而這幾個(gè)變量在多核 CPU 并行的時(shí)候,會(huì)導(dǎo)致競爭緩存行的寫權(quán)限,當(dāng)其中一個(gè) CPU 寫入數(shù)據(jù)后,這個(gè)字段對應(yīng)的緩存行將失效,導(dǎo)致這個(gè)緩存行的其他字段也失效。

在 Disruptor 中,通過填充幾個(gè)無意義的字段,讓對象的大小剛好在 64 字節(jié),一個(gè)緩存行的大小為64字節(jié),這樣這個(gè)緩存行就只會(huì)給這一個(gè)變量使用,從而避免緩存行偽共享,但是在 jdk7 中,由于無效字段被清除導(dǎo)致該方法失效,只能通過繼承父類字段來避免填充字段被優(yōu)化,而 jdk8 提供了注解@Contended 來標(biāo)示這個(gè)變量或?qū)ο髮ⅹ?dú)享一個(gè)緩存行,使用這個(gè)注解必須在 JVM 啟動(dòng)的時(shí)候加上 -XX:-RestrictContended 參數(shù),其實(shí)也是用空間換取時(shí)間。

  1. jdk6  --- 32 位系統(tǒng)下 
  2.     public final static class VolatileLong 
  3.         public volatile long value = 0L; 
  4.         public long p1, p2, p3, p4, p5, p6; // 填充字段 
  5.     } 
  6.  
  7. jdk7 通過繼承 
  8.  
  9.    public class VolatileLongPadding { 
  10.        public volatile long p1, p2, p3, p4, p5, p6; // 填充字段 
  11.    } 
  12.    public class VolatileLong extends VolatileLongPadding { 
  13.        public volatile long value = 0L; 
  14.    } 
  15.  
  16. jdk8 通過注解 
  17.  
  18.    @Contended 
  19.    public class VolatileLong { 
  20.        public volatile long value = 0L; 
  21.    } 

NPTL和 Java 的線程模型

按照教科書的定義,進(jìn)程是資源管理的最小單位,而線程是 CPU 調(diào)度執(zhí)行的最小單位,線程的出現(xiàn)是為了減少進(jìn)程的上下文切換(線程的上下文切換比進(jìn)程小很多),以及更好適配多核心 CPU 環(huán)境,例如一個(gè)進(jìn)程下多個(gè)線程可以分別在不同的 CPU 上執(zhí)行,而多線程的支持,既可以放在Linux內(nèi)核實(shí)現(xiàn),也可以在核外實(shí)現(xiàn),如果放在核外,只需要完成運(yùn)行棧的切換,調(diào)度開銷小,但是這種方式無法適應(yīng)多 CPU 環(huán)境,底層的進(jìn)程還是運(yùn)行在一個(gè) CPU 上,另外由于對用戶編程要求高,所以目前主流的操作系統(tǒng)都是在內(nèi)核支持線程,而在Linux中,線程是一個(gè)輕量級進(jìn)程,只是優(yōu)化了線程調(diào)度的開銷。

而在 JVM 中的線程和內(nèi)核線程是一一對應(yīng)的,線程的調(diào)度完全交給了內(nèi)核,當(dāng)調(diào)用Thread.run 的時(shí)候,就會(huì)通過系統(tǒng)調(diào)用 fork() 創(chuàng)建一個(gè)內(nèi)核線程,這個(gè)方法會(huì)在用戶態(tài)和內(nèi)核態(tài)之間進(jìn)行切換,性能沒有在用戶態(tài)實(shí)現(xiàn)線程高,當(dāng)然由于直接使用內(nèi)核線程,所以能夠創(chuàng)建的最大線程數(shù)也受內(nèi)核控制。目前 Linux上 的線程模型為 NPTL ( Native POSIX Thread Library),他使用一對一模式,兼容 POSIX 標(biāo)準(zhǔn),沒有使用管理線程,可以更好地在多核 CPU 上運(yùn)行。

線程的狀態(tài)

對進(jìn)程而言,就三種狀態(tài),就緒,運(yùn)行,阻塞,而在 JVM 中,阻塞有四種類型,我們可以通過 jstack 生成 dump 文件查看線程的狀態(tài)。

  • BLOCKED (on object monitor) 通過 synchronized(obj) 同步塊獲取鎖的時(shí)候,等待其他線程釋放對象鎖,dump 文件會(huì)顯示 waiting to lock <0x00000000e1c9f108>
  • TIMED WAITING (on object monitor) 和 WAITING (on object monitor) 在獲取鎖后,調(diào)用了 object.wait() 等待其他線程調(diào)用 object.notify(),兩者區(qū)別是是否帶超時(shí)時(shí)間
  • TIMED WAITING (sleeping) 程序調(diào)用了 thread.sleep(),這里如果 sleep(0) 不會(huì)進(jìn)入阻塞狀態(tài),會(huì)直接從運(yùn)行轉(zhuǎn)換為就緒
  • TIMED WAITING (parking) 和 WAITING (parking) 程序調(diào)用了 Unsafe.park(),線程被掛起,等待某個(gè)條件發(fā)生,waiting on condition

而在 POSIX 標(biāo)準(zhǔn)中,thread_block 接受一個(gè)參數(shù) stat ,這個(gè)參數(shù)也有三種類型,TASK_BLOCKED, TASK_WAITING, TASK_HANGING,而調(diào)度器只會(huì)對線程狀態(tài)為 READY 的線程執(zhí)行調(diào)度,另外一點(diǎn)是線程的阻塞是線程自己操作的,相當(dāng)于是線程主動(dòng)讓出 CPU 時(shí)間片,所以等線程被喚醒后,他的剩余時(shí)間片不會(huì)變,該線程只能在剩下的時(shí)間片運(yùn)行,如果該時(shí)間片到期后線程還沒結(jié)束,該線程狀態(tài)會(huì)由 RUNNING 轉(zhuǎn)換為 READY ,等待調(diào)度器的下一次調(diào)度。

好了,關(guān)于線程就分析到這,關(guān)于 Java 并發(fā)包,核心都在 AQS 里,底層是通過 UnSafe類的 cas 方法,以及 park 方法實(shí)現(xiàn),后面我們在找時(shí)間單獨(dú)分析,現(xiàn)在我們在看看 Linux 的進(jìn)程同步方案。

POSIX表示可移植操作系統(tǒng)接口(Portable Operating System Interface of UNIX,縮寫為 POSIX ),POSIX標(biāo)準(zhǔn)定義了操作系統(tǒng)應(yīng)該為應(yīng)用程序提供的接口標(biāo)準(zhǔn)。

CAS 操作需要 CPU 支持,將比較 和 交換 作為一條指令來執(zhí)行, CAS 一般有三個(gè)參數(shù),內(nèi)存位置,預(yù)期原值,新值 ,所以UnSafe 類中的 compareAndSwap 用屬性相對對象初始地址的偏移量,來定位內(nèi)存位置。 

線程的同步

線程同步出現(xiàn)的根本原因是訪問公共資源需要多個(gè)操作,而這多個(gè)操作的執(zhí)行過程不具備原子性,被任務(wù)調(diào)度器分開了,而其他線程會(huì)破壞共享資源,所以需要在臨界區(qū)做線程的同步,這里我們先明確一個(gè)概念,就是臨界區(qū),他是指多個(gè)任務(wù)訪問共享資源如內(nèi)存或文件時(shí)候的指令,他是指令并不是受訪問的資源。

POSIX 定義了五種同步對象,互斥鎖,條件變量,自旋鎖,讀寫鎖,信號量,這些對象在 JVM 中也都有對應(yīng)的實(shí)現(xiàn),并沒有全部使用 POSIX 定義的 api,通過 Java 實(shí)現(xiàn)靈活性更高,也避免了調(diào)用native方法的性能開銷,當(dāng)然底層最終都依賴于 pthread 的 互斥鎖 mutex 來實(shí)現(xiàn),這是一個(gè)系統(tǒng)調(diào)用,開銷很大,所以 JVM 對鎖做了自動(dòng)升降級,基于AQS的實(shí)現(xiàn)以后在分析,這里主要說一下關(guān)鍵字 synchronized 。

當(dāng)聲明 synchronized 的代碼塊時(shí),編譯而成的字節(jié)碼會(huì)包含一個(gè) monitorenter 和 多個(gè) monitorexit (多個(gè)退出路徑,正常和異常情況),當(dāng)執(zhí)行 monitorenter 的時(shí)候會(huì)檢查目標(biāo)鎖對象的計(jì)數(shù)器是否為0,如果為0則將鎖對象的持有線程設(shè)置為自己,然后計(jì)數(shù)器加1,獲取到鎖,如果不為0則檢查鎖對象的持有線程是不是自己,如果是自己就將計(jì)數(shù)器加1獲取鎖,如果不是則阻塞等待,退出的時(shí)候計(jì)數(shù)器減1,當(dāng)減為0的時(shí)候清楚鎖對象的持有線程標(biāo)記,可以看出 synchronized 是支持可重入的。

剛剛說到線程的阻塞是一個(gè)系統(tǒng)調(diào)用,開銷大,所以 JVM 設(shè)計(jì)了自適應(yīng)自旋鎖,就是當(dāng)沒有獲取到鎖的時(shí)候, CPU 回進(jìn)入自旋狀態(tài)等待其他線程釋放鎖,自旋的時(shí)間主要看上次等待多長時(shí)間獲取的鎖,例如上次自旋5毫秒沒有獲取鎖,這次就6毫秒,自旋會(huì)導(dǎo)致 CPU 空跑,另一個(gè)副作用就是不公平的鎖機(jī)制,因?yàn)樵摼€程自旋獲取到鎖,而其他正在阻塞的線程還在等待。除了自旋鎖, JVM 還通過 CAS 實(shí)現(xiàn)了輕量級鎖和偏向鎖來分別針對多個(gè)線程在不同時(shí)間訪問鎖和鎖僅會(huì)被一個(gè)線程使用的情況。后兩種鎖相當(dāng)于并沒有調(diào)用底層的信號量實(shí)現(xiàn)(通過信號量來控制線程A釋放了鎖例如調(diào)用了 wait(),而線程B就可以獲取鎖,這個(gè)只有內(nèi)核才能實(shí)現(xiàn),后面兩種由于場景里沒有競爭所以也就不需要通過底層信號量控制),只是自己在用戶空間維護(hù)了鎖的持有關(guān)系,所以更高效。

如上圖所示,如果線程進(jìn)入 monitorenter 會(huì)將自己放入該 objectmonitor 的 entryset 隊(duì)列,然后阻塞,如果當(dāng)前持有線程調(diào)用了 wait 方法,將會(huì)釋放鎖,然后將自己封裝成 objectwaiter 放入 objectmonitor 的 waitset 隊(duì)列,這時(shí)候 entryset 隊(duì)列里的某個(gè)線程將會(huì)競爭到鎖,并進(jìn)入 active 狀態(tài),如果這個(gè)線程調(diào)用了 notify 方法,將會(huì)把 waitset 的第一個(gè) objectwaiter 拿出來放入 entryset (這個(gè)時(shí)候根據(jù)策略可能會(huì)先自旋),當(dāng)調(diào)用 notify 的那個(gè)線程執(zhí)行 moniterexit 釋放鎖的時(shí)候, entryset 里的線程就開始競爭鎖后進(jìn)入 active 狀態(tài)。

為了讓應(yīng)用程序免于數(shù)據(jù)競爭的干擾, Java 內(nèi)存模型中定義了 happen-before 來描述兩個(gè)操作的內(nèi)存可見性,也就是 X 操作 happen-before 操作 Y , 那么 X 操作結(jié)果 對 Y 可見。

JVM 中針對 volatile 以及 鎖 的實(shí)現(xiàn)有 happen-before 規(guī)則, JVM 底層通過插入內(nèi)存屏障來限制編譯器的重排序,以 volatile 為例,內(nèi)存屏障將不允許 在 volatile 字段寫操作之前的語句被重排序到寫操作后面 , 也不允許讀取 volatile 字段之后的語句被重排序帶讀取語句之前。插入內(nèi)存屏障的指令,會(huì)根據(jù)指令類型不同有不同的效果,例如在 monitorexit 釋放鎖后會(huì)強(qiáng)制刷新緩存,而 volatile 對應(yīng)的內(nèi)存屏障會(huì)在每次寫入后強(qiáng)制刷新到主存,并且由于 volatile 字段的特性,編譯器無法將其分配到寄存器,所以每次都是從主存讀取,所以 volatile 適用于讀多寫少得場景,最好只有個(gè)線程寫多個(gè)線程讀,如果頻繁寫入導(dǎo)致不停刷新緩存會(huì)影響性能。

關(guān)于應(yīng)用程序中設(shè)置多少線程數(shù)合適的問題,我們一般的做法是設(shè)置 CPU 最大核心數(shù) * 2 ,我們編碼的時(shí)候可能不確定運(yùn)行在什么樣的硬件環(huán)境中,可以通過 Runtime.getRuntime().availableProcessors() 獲取 CPU 核心。

但是具體設(shè)置多少線程數(shù),主要和線程內(nèi)運(yùn)行的任務(wù)中的阻塞時(shí)間有關(guān)系,如果任務(wù)中全部是計(jì)算密集型,那么只需要設(shè)置 CPU 核心數(shù)的線程就可以達(dá)到 CPU 利用率最高,如果設(shè)置的太大,反而因?yàn)榫€程上下文切換影響性能,如果任務(wù)中有阻塞操作,而在阻塞的時(shí)間就可以讓 CPU 去執(zhí)行其他線程里的任務(wù),我們可以通過 線程數(shù)量=內(nèi)核數(shù)量 / (1 - 阻塞率)這個(gè)公式去計(jì)算最合適的線程數(shù),阻塞率我們可以通過計(jì)算任務(wù)總的執(zhí)行時(shí)間和阻塞的時(shí)間獲得。

目前微服務(wù)架構(gòu)下有大量的RPC調(diào)用,所以利用多線程可以大大提高執(zhí)行效率,我們可以借助分布式鏈路監(jiān)控來統(tǒng)計(jì)RPC調(diào)用所消耗的時(shí)間,而這部分時(shí)間就是任務(wù)中阻塞的時(shí)間,當(dāng)然為了做到極致的效率最大,我們需要設(shè)置不同的值然后進(jìn)行測試。 

Java 中如何實(shí)現(xiàn)定時(shí)任務(wù)

定時(shí)器已經(jīng)是現(xiàn)代軟件中不可缺少的一部分,例如每隔5秒去查詢一下狀態(tài),是否有新郵件,實(shí)現(xiàn)一個(gè)鬧鐘等, Java 中已經(jīng)有現(xiàn)成的 api 供使用,但是如果你想設(shè)計(jì)更高效,更精準(zhǔn)的定時(shí)器任務(wù),就需要了解底層的硬件知識,比如實(shí)現(xiàn)一個(gè)分布式任務(wù)調(diào)度中間件,你可能要考慮到各個(gè)應(yīng)用間時(shí)鐘同步的問題。

Java 中我們要實(shí)現(xiàn)定時(shí)任務(wù),有兩種方式,一種通過 timer 類, 另外一種是 JUC 中的 ScheduledExecutorService ,不知道大家有沒有好奇 JVM 是如何實(shí)現(xiàn)定時(shí)任務(wù)的,難道一直輪詢時(shí)間,看是否時(shí)間到了,如果到了就調(diào)用對應(yīng)的處理任務(wù),但是這種一直輪詢不釋放 CPU 肯定是不可取的,要么就是線程阻塞,等到時(shí)間到了在來喚醒線程,那么 JVM 怎么知道時(shí)間到了,如何喚醒呢?

首先我們翻一下 JDK ,發(fā)現(xiàn)和時(shí)間相關(guān)的 API 大概有3處,而且這 3 處還都對時(shí)間的精度做了區(qū)分:

object.wait(long millisecond) 參數(shù)是毫秒,必須大于等于 0 ,如果等于 0 ,就一直阻塞直到其他線程來喚醒 ,timer 類就是通過 wait() 方法來實(shí)現(xiàn),下面我們看一下wait的另外一個(gè)方法:

  1. public final void wait(long timeout, int nanos) throws InterruptedException { 
  2.      if (timeout < 0) { 
  3.          throw new IllegalArgumentException("timeout value is negative"); 
  4.      } 
  5.      if (nanos < 0 || nanos > 999999) { 
  6.          throw new IllegalArgumentException( 
  7.                              "nanosecond timeout value out of range"); 
  8.      } 
  9.      if (nanos > 0) { 
  10.          timeout++; 
  11.      } 
  12.      wait(timeout); 
  13.  } 

這個(gè)方法是想提供一個(gè)可以支持納秒級的超時(shí)時(shí)間,然而只是粗暴的加 1 毫秒。

Thread.sleep(long millisecond) 目前一般通過這種方式釋放 CPU ,如果參數(shù)為 0 ,表示釋放 CPU 給更高優(yōu)先級的線程,自己從運(yùn)行狀態(tài)轉(zhuǎn)換為可運(yùn)行態(tài)等待 CPU 調(diào)度,他也提供了一個(gè)可以支持納秒級的方法實(shí)現(xiàn),跟 wait 額區(qū)別是它通過 500000 來分隔是否要加 1 毫秒。

  1. public static void sleep(long millis, int nanos) 
  2.  throws InterruptedException { 
  3.      if (millis < 0) { 
  4.          throw new IllegalArgumentException("timeout value is negative"); 
  5.      } 
  6.      if (nanos < 0 || nanos > 999999) { 
  7.          throw new IllegalArgumentException( 
  8.                              "nanosecond timeout value out of range"); 
  9.      } 
  10.      if (nanos >= 500000 || (nanos != 0 && millis == 0)) { 
  11.          millis++; 
  12.      } 
  13.      sleep(millis); 
  14.  } 

LockSupport.park(long nans) Condition.await()調(diào)用的該方法, ScheduledExecutorService 用的 condition.await() 來實(shí)現(xiàn)阻塞一定的超時(shí)時(shí)間,其他帶超時(shí)參數(shù)的方法也都通過他來實(shí)現(xiàn),目前大多定時(shí)器都是通過這個(gè)方法來實(shí)現(xiàn)的,該方法也提供了一個(gè)布爾值來確定時(shí)間的精度。

System.currentTimeMillis() 以及 System.nanoTime() 這兩種方式都依賴于底層操作系統(tǒng),前者是毫秒級,經(jīng)測試 windows 平臺(tái)的頻率可能超過 10ms ,而后者是納秒級別,頻率在 100ns 左右,所以如果要獲取更精準(zhǔn)的時(shí)間建議用后者好了,api 了解完了,我們來看下定時(shí)器的底層是怎么實(shí)現(xiàn)的,現(xiàn)代PC機(jī)中有三種硬件時(shí)鐘的實(shí)現(xiàn),他們都是通過晶體振動(dòng)產(chǎn)生的方波信號輸入來完成時(shí)鐘信號同步的。

  • 實(shí)時(shí)時(shí)鐘 RTC ,用于長時(shí)間存放系統(tǒng)時(shí)間的設(shè)備,即使關(guān)機(jī)也可以依靠主板中的電池繼續(xù)計(jì)時(shí)。Linux 啟動(dòng)的時(shí)候會(huì)從 RTC 中讀取時(shí)間和日期作為初始值,之后在運(yùn)行期間通過其他計(jì)時(shí)器去維護(hù)系統(tǒng)時(shí)間。
  • 可編程間隔定時(shí)器 PIT ,該計(jì)數(shù)器會(huì)有一個(gè)初始值,每過一個(gè)時(shí)鐘周期,該初始值會(huì)減1,當(dāng)該初始值被減到0時(shí),就通過導(dǎo)線向 CPU 發(fā)送一個(gè)時(shí)鐘中斷, CPU 就可以執(zhí)行對應(yīng)的中斷程序,也就是回調(diào)對應(yīng)的任務(wù)
  • 時(shí)間戳計(jì)數(shù)器 TSC , 所有的 Intel8086 CPU 中都包含一個(gè)時(shí)間戳計(jì)數(shù)器對應(yīng)的寄存器,該寄存器的值會(huì)在每次 CPU 收到一個(gè)時(shí)鐘周期的中斷信號后就會(huì)加 1 。他比 PIT 精度高,但是不能編程,只能讀取。

時(shí)鐘周期:硬件計(jì)時(shí)器在多長時(shí)間內(nèi)產(chǎn)生時(shí)鐘脈沖,而時(shí)鐘周期頻率為1秒內(nèi)產(chǎn)生時(shí)鐘脈沖的個(gè)數(shù)。目前通常為1193180。

時(shí)鐘滴答:當(dāng)PIT中的初始值減到0的時(shí)候,就會(huì)產(chǎn)生一次時(shí)鐘中斷,這個(gè)初始值由編程的時(shí)候指定。 

Linux啟動(dòng)的時(shí)候,先通過 RTC 獲取初始時(shí)間,之后內(nèi)核通過 PIT 中的定時(shí)器的時(shí)鐘滴答來維護(hù)日期,并且會(huì)定時(shí)將該日期寫入 RTC,而應(yīng)用程序的定時(shí)器主要是通過設(shè)置 PIT 的初始值設(shè)置的,當(dāng)初始值減到0的時(shí)候,就表示要執(zhí)行回調(diào)函數(shù)了,這里大家會(huì)不會(huì)有疑問,這樣同一時(shí)刻只能有一個(gè)定時(shí)器程序了,而我們在應(yīng)用程序中,以及多個(gè)應(yīng)用程序之間,肯定有好多定時(shí)器任務(wù),其實(shí)我們可以參考 ScheduledExecutorService 的實(shí)現(xiàn)。

只需要將這些定時(shí)任務(wù)按照時(shí)間做一個(gè)排序,越靠前待執(zhí)行的任務(wù)放在前面,第一個(gè)任務(wù)到了在設(shè)置第二個(gè)任務(wù)相對當(dāng)前時(shí)間的值,畢竟 CPU 同一時(shí)刻也只能運(yùn)行一個(gè)任務(wù),關(guān)于時(shí)間的精度問題,我們無法在軟件層面做的完全精準(zhǔn),畢竟 CPU 的調(diào)度不完全受用戶程序控制,當(dāng)然更大的依賴是硬件的時(shí)鐘周期頻率,目前 TSC 可以提高更高的精度。

現(xiàn)在我們知道了,Java 中的超時(shí)時(shí)間,是通過可編程間隔定時(shí)器設(shè)置一個(gè)初始值然后等待中斷信號實(shí)現(xiàn)的,精度上受硬件時(shí)鐘周期的影響,一般為毫秒級別,畢竟1納秒光速也只有3米,所以 JDK 中帶納秒?yún)?shù)的實(shí)現(xiàn)都是粗暴做法,預(yù)留著等待精度更高的定時(shí)器出現(xiàn),而獲取當(dāng)前時(shí)間 System.currentTimeMillis() 效率會(huì)更高,但他是毫秒級精度,他讀取的 Linux 內(nèi)核維護(hù)的日期,而 System.nanoTime() 會(huì)優(yōu)先使用 TSC ,性能稍微低一點(diǎn),但他是納秒級,Random 類為了防止沖突就用nanoTime生成種子。

Java 如何和外部設(shè)備通信

計(jì)算機(jī)的外部設(shè)備有鼠標(biāo)、鍵盤、打印機(jī)、網(wǎng)卡等,通常我們將外部設(shè)備和和主存之間的信息傳遞稱為 I/O 操作 , 按操作特性可以分為,輸出型設(shè)備,輸入型設(shè)備,存儲(chǔ)設(shè)備。現(xiàn)代設(shè)備都采用通道方式和主存進(jìn)行交互,通道是一個(gè)專門用來處理IO任務(wù)的設(shè)備, CPU 在處理主程序時(shí)遇到I/O請求,啟動(dòng)指定通道上選址的設(shè)備,一旦啟動(dòng)成功,通道開始控制設(shè)備進(jìn)行操作,而 CPU 可以繼續(xù)執(zhí)行其他任務(wù),I/O 操作完成后,通道發(fā)出 I/O 操作結(jié)束的中斷,處理器轉(zhuǎn)而處理 IO 結(jié)束后的事件。其他處理 IO 的方式,例如輪詢、中斷、DMA,在性能上都不見通道,這里就不介紹了。當(dāng)然 Java 程序和外部設(shè)備通信也是通過系統(tǒng)調(diào)用完成,這里也不在繼續(xù)深入了。

 

責(zé)任編輯:武曉燕 來源: 阿里技術(shù)
相關(guān)推薦

2021-04-15 18:44:15

2009-12-17 14:56:32

Linux程序設(shè)計(jì)

2019-12-13 09:55:27

網(wǎng)絡(luò)安全5G技術(shù)

2013-12-18 10:34:42

OpenMP線程

2022-08-26 08:35:59

對象設(shè)計(jì)底層

2020-12-09 09:39:52

SaaSLTV軟件

2014-10-30 10:09:44

程序員程序設(shè)計(jì)師

2016-03-17 16:57:39

SaaSSaaS公司指標(biāo)

2013-12-12 16:30:20

Lua腳本語言

2018-08-20 13:39:15

小程序設(shè)計(jì)UI設(shè)計(jì)師

2009-12-04 10:53:06

VS WEB

2010-12-28 10:12:39

PHP

2011-07-05 16:05:43

面向?qū)ο缶幊?/a>

2011-07-22 13:41:57

java

2011-07-05 15:22:04

程序設(shè)計(jì)

2009-06-23 17:52:04

Linux程序設(shè)計(jì)

2011-07-05 15:59:57

面向?qū)ο缶幊?/a>

2009-06-23 18:13:21

2012-01-11 13:37:37

程序員

2014-04-16 11:39:52

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號