java硬件编程_关于JAVA并发编程你需要知道的——硬件篇
無論程序語言如何千變萬化,他們都深深地根植于目前的計算機體系結構。
左圖是intel CPU的三級高速緩存設計,由于高速緩存對程序員基本不可見,因此可以抽象為右圖。
緩存的設計
首先還是先談談左圖。
L1-cache分為兩部分,i-cache存儲指令(只讀),d-cache存儲數據(可讀可寫)
CPU只能和寄存器以及L1-cache進行直接交互,數據不能隔層傳遞,只能一層一層往上讀,一層一層往下寫
訪問L1需要至少4個時鐘周期,L2需要至少10個,L3需要至少30個。即便是速度最快的L1,也低于運算單元的執行速度,何況存在緩存未命中的情況,因此在L1和運算單元之間加上了Writebuffer和Readbuffer(合稱Memory Ordering Buffer,MOB),數據準備好的時候再完成相關指令,這就是CPU指令亂序——順序執行亂序完成。亂序完成的結果放入到Writebuffer中,按照原有的執行順序,刷到緩存中
緩存由多個緩存行組成。每個緩存行結構如圖所示(以64位機器為例)
CPU讀取緩存的時候找到對應的緩存行,如果前面的有效位為零,就從下一級緩存加載到這一級緩存
相關的問題
明白緩存的設計之后,再看右圖來分析其中的問題
緩存導致的內存可見性:已知線程A運行在core 0上,線程B運行在core 1上,兩者都對同一個內存地址進行讀取,這個內存地址的內容會被加載到cache,然后CPU讀取,這時候線程A對內容進行了修改,但是線程B卻可能一直從本核心的cache讀取,無法感知到該地址的內容已被修改。
多核導致的自增操作原子性:自增操作分為三步:從內存讀取變量到寄存器;寄存器中的值加1;寫回到內存。已知線程A運行在core 0上,線程B運行在core 1上,兩者都對變量執行加一操作。A執行完一二兩步時,B執行完第一步,A將加一后的值寫入到內存,B執行完二三兩步也將加一后的值寫入到內存,結果變量只加了一,而不是加二
MOB導致的cache可見性:a=1.0; a=a/2; a=a-1.0;按照正常的邏輯,a最后的結果為-0.5;但是因為除法的執行時鐘周期大于減法,第三句執行時,a/2的結果存放在writebuffer中還沒寫入到緩存,a-1.0中a的值已經從緩存中加載到readbuffer,也就是a-1.0=1.0-1.0=0 (高級語言不會出現這個問題,因為編譯器已經做了處理,前面的偽代碼僅表示邏輯)
相關的實現
為了解決這些問題,CPU提供了一些指令,其中比如lock和cmpxchg。
lock 匯編前綴,在Intel奔騰系列之前,這個指令前綴能夠鎖定總線,禁止其他CPU核心操作內存,執行完后邊的指令后釋放總線,在這個過程中其他CPU核心會監聽總線,發現某個內存地址內容被修改,就會將本核心下的對應cache行有效位置0。因為鎖總線會禁止所有內存操作,降低效率,因此在奔騰之后,這個指令前綴不鎖總線而是鎖定相關的cache行,對某個地址修改后直接讓相關cache失效。這樣解決了問題一
cmpxchg 將寄存器a中的值與內存中比較如果一樣,將寄存器c中的值和內存中的值交換,如果不一樣就設置異常位并將內存中的值讀取到寄存器a。代入到問題二,從內存讀取值到寄存器a,加一后保存到寄存器c,然后執行cmpxchg,執行完后如果有異常,就重新加一,再嘗試寫回,直到成功。這樣解決了問題二
cmpxchg和lock 在執行完以后會將writebuffer刷到cache并清空readbuffer,這樣解決了問題三。另外X86_64引入了內存屏障指令 lfence、sfence、mfence。lfence前面的讀取操作完成,也就是readbuffer中的內容全部被cpu讀取后,才能執行lfence之后的讀取操作;sfence前面的寫入操作完成,也就是writebuffer全部刷到緩存中,才能執行sfence之后的寫入操作;mfence之前的讀寫操作全部完成,才能進行mfence之后的操作。
總結
以上是生活随笔為你收集整理的java硬件编程_关于JAVA并发编程你需要知道的——硬件篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 企业信息安全整体架构
- 下一篇: 2.7万字还原行业面貌,《2019 AI