硬件相關(guān)知識(shí)
馮諾依曼模型
1945年桥滨,馮諾依曼和其他計(jì)算機(jī)科學(xué)家提出了計(jì)算機(jī)具體實(shí)現(xiàn)的報(bào)告,遵循了圖靈機(jī)的設(shè)計(jì)弛车,提出用 電子元件 構(gòu)造計(jì)算機(jī)齐媒,并約定用二進(jìn)制進(jìn)行計(jì)算和存儲(chǔ),將計(jì)算機(jī)基本結(jié)構(gòu)分為5部分:中央處理器纷跛,內(nèi)存喻括,輸入設(shè)備,輸出設(shè)備贫奠,總線
多核心CPU和多個(gè)CPU
假設(shè)現(xiàn)在我們要設(shè)計(jì)一臺(tái)計(jì)算機(jī)的處理器部分的[架構(gòu)]我們有兩種選擇唬血,多個(gè)單核CPU和單個(gè)多核CPU
如果我們選擇多個(gè)單核CPU望蜡,那么每一個(gè)CPU都需要有較為獨(dú)立的電路支持,有自己的Cache拷恨,而他們之間通過板上的總線進(jìn)行通信脖律。在這樣的架構(gòu)上,我們要跑一個(gè)[多線程]的程序(常見典型情況)腕侄,不考慮超線程小泉,那么每一個(gè)線程就要跑在一個(gè)獨(dú)立的CPU上,線程間的所有協(xié)作都要走總線兜挨,而共享的數(shù)據(jù)更是有可能要在好幾個(gè)Cache里同時(shí)存在膏孟。這樣的話,總線開銷相比較而言是很大的拌汇,怎么辦柒桑?那么多Cache,即使我們不心疼存儲(chǔ)能力的浪費(fèi)噪舀,一致性怎么保證魁淳?如果真正做出來,還要在主板上占多塊地盤与倡,給布局布線帶來更大的挑戰(zhàn)界逛;
如果我們選擇多核單CPU,那么我們只需要一套芯片組纺座,一套存儲(chǔ)息拜,多核之間通過芯片內(nèi)部總線進(jìn)行通信,共享使用內(nèi)存净响。在這樣的架構(gòu)上少欺,如果我們跑一個(gè)多線程的程序,那么線程間通信將比上一種情形更快馋贤。如果最終實(shí)現(xiàn)出來赞别,對(duì)板上空間的占用較小,布局布線的壓力也較小配乓。
但是仿滔,如果需要同時(shí)跑多個(gè)大程序怎么辦?假設(shè)倆大程序犹芹,每一個(gè)程序都好多線程還幾乎用滿cache崎页,它們分時(shí)使用CPU,那在程序間切換的時(shí)候腰埂,光指令和數(shù)據(jù)的替換就要費(fèi)多大事情笆底颉!
所以呢盐固,大部分一般咱們使用的電腦荒给,都是單CPU多核的,比如我們配的Dell T3600刁卜,有一顆Intel Xeon E5-1650志电,6核,虛擬為12個(gè)邏輯核心蛔趴。少部分高端人士需要更強(qiáng)的多任務(wù)并發(fā)能力挑辆,就會(huì)搞一個(gè)多顆多核CPU的機(jī)子,Mac Pro就可以有兩顆孝情。
-
多核CPU
多核CPU即1個(gè)CPU有多個(gè)核心鱼蝉,可以理解為是多個(gè)CPU,這些CPU集成在一個(gè)芯片里箫荡,可以通過內(nèi)部總線來交互數(shù)據(jù)魁亦,共享數(shù)據(jù),這些CPU中分配出一個(gè)獨(dú)立的核執(zhí)行操作系統(tǒng)羔挡,每個(gè)核都有自己的寄存器洁奈,alu運(yùn)算單元等(這些都是封裝在cpu內(nèi)部的)
-
多個(gè)CPU
四核處理器解讀:
四核處理器即是基于單個(gè)半導(dǎo)體的一個(gè)處理器上擁有四個(gè)一樣功能的處理器核心。換句話說绞灼,將四個(gè)物理處理器核心整合入一個(gè)核中
多核心處理器利术,任務(wù)執(zhí)行的那一小段時(shí)間叫做時(shí)間片,任務(wù)正在執(zhí)行時(shí)的狀態(tài)叫運(yùn)行狀態(tài)低矮,被暫停的線程任務(wù)狀態(tài)叫做就緒狀態(tài)印叁,意為等待下一個(gè)屬于它的時(shí)間片的到來
Java內(nèi)存模型
多核心并發(fā)緩存架構(gòu)
一個(gè)CPU里面有兩個(gè)核心,可以理解為2個(gè)cpu军掂,所有數(shù)據(jù)在硬盤轮蜕,計(jì)算機(jī)要運(yùn)行程序,cpu將會(huì)把數(shù)據(jù)加載到主內(nèi)存(內(nèi)存條)
很久以前的老計(jì)算機(jī)良姆,cpu是和主內(nèi)存直接交互的肠虽,根據(jù)摩爾定律:微處理器的性能每隔18個(gè)月提高一倍,或價(jià)格下降一半玛追,但是主內(nèi)存不是税课,所以如果cpu跟主內(nèi)存直接交互,cpu速度非橙剩快韩玩,主內(nèi)存速度很慢,那么總的速度以慢的為主
解決方案:CPU高速緩存
現(xiàn)代計(jì)算機(jī)都會(huì)在cpu和主內(nèi)存之間架設(shè)一級(jí)緩存陆馁,即cpu高速緩存
L1,L2,L3就是我們的cpu的高速緩存找颓,即CPU緩存,嚴(yán)格來說在cpu內(nèi)部叮贩,價(jià)格非常昂貴击狮,其容量遠(yuǎn)小于主內(nèi)存佛析,但速度卻可以接近處理器的頻率,之所以加cpu緩存就是為了解決cpu和主內(nèi)存運(yùn)算速度不一致的問題彪蓬。CPU緩存一般直接跟CPU芯片集成或位于主板總線互連的獨(dú)立芯片上寸莫。
隨著多核CPU的發(fā)展,CPU緩存通常分成了三個(gè)級(jí)別:L1档冬,L2膘茎,L3。級(jí)別越小越接近CPU酷誓,所以速度也更快披坏,同時(shí)也代表著容量越小。L1 是最接近CPU的, 它容量最醒问(例如:32K)棒拂,速度最快,每個(gè)核上都有一個(gè) L1 緩存娘扩,L1 緩存每個(gè)核上其實(shí)有兩個(gè) L1 緩存, 一個(gè)用于存數(shù)據(jù)的 L1d Cache(Data Cache)着茸,一個(gè)用于存指令的 L1i Cache(Instruction Cache)。L2 緩存 更大一些(例如:256K)琐旁,速度要慢一些, 一般情況下每個(gè)核上都有一個(gè)獨(dú)立的L2 緩存; L3 緩存是三級(jí)緩存中最大的一級(jí)(例如3MB)涮阔,同時(shí)也是最慢的一級(jí), 在同一個(gè)CPU插槽之間的核共享一個(gè) L3 緩存。
讀取數(shù)據(jù)過程灰殴。就像數(shù)據(jù)庫緩存一樣敬特,首先在最快的緩存中找數(shù)據(jù),如果緩存沒有命中(Cache miss) 則往下一級(jí)找, 直到三級(jí)緩存都找不到時(shí)牺陶,向內(nèi)存要數(shù)據(jù)伟阔。一次次地未命中,代表取數(shù)據(jù)消耗的時(shí)間越長(zhǎng)掰伸。
有了高速緩存以后皱炉,先把我們的數(shù)據(jù)從硬盤上加載到主內(nèi)存,再把數(shù)據(jù)從主內(nèi)存加載到cpu緩存狮鸭,那么cpu再做存取的時(shí)候就是跟我們的cpu緩存進(jìn)行交互
JMM
java線程之間通信是通過JMM控制的合搅!
java線程內(nèi)存模型跟CPU緩存模型類似,是基于cpu緩存模型來建立的歧蕉,java線程內(nèi)存模型是標(biāo)準(zhǔn)化的灾部,屏蔽掉了底層不同計(jì)算機(jī)的區(qū)別
假設(shè)當(dāng)前是多核cpu,有多個(gè)線程在同時(shí)運(yùn)行惯退,每個(gè)線程運(yùn)行在不同的cpu上赌髓,我們假設(shè)他是并行執(zhí)行的
多個(gè)線程來讀取共享變量(比如static變量,或者一個(gè)對(duì)象的公共實(shí)例變量),首先是把主內(nèi)存中的公共變量加載到各自線程的工作內(nèi)存锁蠕,然后在每個(gè)線程里面做自己的運(yùn)算夷野,如果線程A把initFlag改為true了,線程B很可能感知不到這個(gè)變化匿沛,還是false
這個(gè)線程的工作內(nèi)存跟cpu的高速緩存很類似扫责,為了提高運(yùn)算速度,會(huì)為每個(gè)線程搞一個(gè)工作內(nèi)存逃呼,雖然提高了性能,但是也給程序帶來了可能出現(xiàn)bug的問題
注意:本地內(nèi)存并不真實(shí)存在者娱,它是個(gè)JMM內(nèi)存模型的抽象概念抡笼,狹義理解為CPU高速緩存器和寄存器。
寄存器是中央處理器內(nèi)的組成部份黄鳍。它跟CPU有關(guān)推姻。寄存器是有限存貯容量的高速存貯部件,它們可用來暫存指令框沟、數(shù)據(jù)和位址藏古。在中央處理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序計(jì)數(shù)器(PC)忍燥。在中央處理器的算術(shù)及邏輯部件中拧晕,包含的寄存器有累加器(ACC)。
可見性測(cè)試
public class VisualTest {
public static volatile boolean initFlag = false;
public static void main(String[] args) {
Thread t1= new Thread(() -> {
// System.out.println("等待數(shù)據(jù)");
while(!initFlag){
}
// System.out.println("處理結(jié)束");
});
t1.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2= new Thread(() -> {
prepareData();
});
t2.start();
}
public static void prepareData(){
System.out.println("準(zhǔn)備數(shù)據(jù)中...");
initFlag = true;
System.out.println("準(zhǔn)備數(shù)據(jù)結(jié)束");
}
}
按道理來說static是共享的梅垄,第二個(gè)線程修改了flag變量厂捞,第一個(gè)程序按理說應(yīng)該退出,但是結(jié)果是
沒有退出打印處理結(jié)束队丝,原因是一號(hào)線程沒有感知到二號(hào)線程對(duì)flag變量的值的修改靡馁,要讓他感知到,需要在flag前面加上volatile机久,下面我們加上volatile再進(jìn)行測(cè)試臭墨,發(fā)現(xiàn)程序成功退出
JMM內(nèi)存原子操作
關(guān)于主內(nèi)存與工作內(nèi)存之間的交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存膘盖。如何從工作內(nèi)存同步到主內(nèi)存中的實(shí)現(xiàn)細(xì)節(jié)胧弛。java內(nèi)存模型定義了8種操作來完成
下面結(jié)合jmm內(nèi)存原子操作剖析一下沒加volatile之前為什么會(huì)出現(xiàn)線程操作不可見問題
注意:上圖右邊應(yīng)該是線程2,圖標(biāo)記錯(cuò)了
靜態(tài)變量在類加載的時(shí)候就賦值了,false衔憨,主內(nèi)存就已經(jīng)有靜態(tài)變量這個(gè)值
線程1會(huì)將主內(nèi)存中的flag和值拷貝到工作內(nèi)存一份叶圃,然后再進(jìn)行一些操作,線程1對(duì)工作內(nèi)存中的變量副本的運(yùn)算use就是取反判斷践图,然后在cpu那里做where死循環(huán)掺冠,也就是第一個(gè)線程一直卡在cpu那里一直空轉(zhuǎn)
線程2將主內(nèi)存的flag和值拷貝到工作內(nèi)存一份,然后線程2對(duì)工作內(nèi)存中的變量副本執(zhí)行運(yùn)算use操作,在上面的例子中就是修改他的值德崭,修改完后斥黑,將新的值assign賦值回線程2的工作內(nèi)存,然后再將這個(gè)新的值寫到主內(nèi)存眉厨,最后執(zhí)行write操作锌奴,將這新值賦值給主內(nèi)存中的initFlag(上面圖中的store那一步,實(shí)際上已經(jīng)指向的那個(gè)initFlag=true進(jìn)入了主內(nèi)存里面憾股,上面放在外面是為了方便演示)
但是問題是線程1還卡在where那里死循環(huán)鹿蜀,它并沒有獲取到這個(gè)改變,導(dǎo)致程序無法退出
JMM緩存不一致問題解決方案
- 總線加鎖(類似于加了一把悲觀鎖)
操作系統(tǒng)提供了總線鎖定的機(jī)制。前端總線(也叫CPU總線)是所有CPU與芯片組連接的主干道,負(fù)責(zé)CPU與外界所有部件的通信肴沫,包括高速緩存锨天、內(nèi)存、北橋,其控制總線向各個(gè)部件發(fā)送控制信號(hào)、通過地址總線發(fā)送地址信號(hào)指定其要訪問的部件、通過數(shù)據(jù)總線雙向傳輸分冈。在CPU1要做 i++操作的時(shí)候,其在總線上發(fā)出一個(gè)LOCK#信號(hào)霸株,其他處理器就不能操作緩存了該共享變量?jī)?nèi)存地址的緩存雕沉,也就是阻塞了其他CPU,使該處理器可以獨(dú)享此共享內(nèi)存淳衙。
只要有一個(gè)線程先讀取到主內(nèi)存中的共享變量到工作副本以后蘑秽,就給主內(nèi)存這個(gè)共享變量加鎖,直到那個(gè)線程使用結(jié)束箫攀,釋放鎖肠牲,其他等待的線程要獲取這個(gè)共享變量的話是獲取不到的
如果多個(gè)線程不同時(shí)讀取這個(gè)共享變量的話,他們是可以并行操作的靴跛,一旦他們讀取共享變量的話initFlag的時(shí)候缀雳,會(huì)在主內(nèi)存那里排隊(duì),誰先拿到這個(gè)變量梢睛,就在這里加一把鎖
那么第二個(gè)線程能獲取到鎖的時(shí)候肥印,這個(gè)值必定是修改過的,但是這種做法無疑非常影響性能绝葡,現(xiàn)代計(jì)算機(jī)采用MESI緩存一致性來解決JMM緩存不一致問題
- mesi緩存一致性協(xié)議
狀態(tài) | 描述 |
---|---|
M(Modified) | 這行數(shù)據(jù)有效深碱,數(shù)據(jù)被修改了,和內(nèi)存中的數(shù)據(jù)不一致藏畅,數(shù)據(jù)只存在于本Cache中 |
E(Exclusive) | 這行數(shù)據(jù)有效敷硅,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致,數(shù)據(jù)只存在于本Cache中 |
S(Shared) | 這行數(shù)據(jù)有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致绞蹦,數(shù)據(jù)存在于很多Cache中 |
I(Invalid) | 這行數(shù)據(jù)無效 |
我們的cpu跟我們的主內(nèi)存之間交互力奋,最后都是通過總線,總線是用來連接多個(gè)硬件(cpu幽七,主內(nèi)存景殷,主板)的組件
當(dāng)線程2把共享變量initFlag的值修改了以后,當(dāng)他同步回主內(nèi)存的時(shí)候要通過總線(數(shù)據(jù)最終是要通過總線傳回主內(nèi)存的)澡屡,而不管有多少個(gè)cpu猿挚,都會(huì)對(duì)總線嗅探(監(jiān)聽),監(jiān)聽的是他感興趣的數(shù)據(jù)挪蹭,比如線程1的工作內(nèi)存中有initFlag這個(gè)數(shù)據(jù)亭饵,那他就會(huì)監(jiān)聽我們這個(gè)變量的改變,一旦線程2對(duì)變量的更改通過總線的時(shí)候梁厉,線程1就會(huì)監(jiān)聽到這個(gè)變量的改變,然后讓自己工作內(nèi)存中的這個(gè)變量失效
然后線程1的cpu讀這個(gè)值的時(shí)候踏兜,發(fā)現(xiàn)這個(gè)變量的內(nèi)存地址里面空掉了词顾,沒有任何值了,那么他會(huì)馬上從主內(nèi)存里進(jìn)行read操作碱妆,這時(shí)候這個(gè)值已經(jīng)改變了肉盹,就可以正常進(jìn)行了
volatile
volatile緩存可見性原理
底層通過匯編lock前綴指令來實(shí)現(xiàn)
lock指令完成兩件事:
- 立即將當(dāng)前處理器工作內(nèi)存中的數(shù)據(jù)寫回到主內(nèi)存
- 這個(gè)寫回內(nèi)存的操作會(huì)引起在其他內(nèi)存中緩存了該內(nèi)存地址的數(shù)據(jù)無效(MESI協(xié)議)
直接點(diǎn)擊volatile是看不了源碼的,因?yàn)関olatile關(guān)鍵字底層不是java實(shí)現(xiàn)的疹尾,是C上忍,如果底層是java代碼實(shí)現(xiàn)的,你直接是可以點(diǎn)進(jìn)去的纳本,我們通過查看上述 VisualTest代碼的底層匯編語言(低級(jí)語言窍蓝,寫起來復(fù)雜,但是可以看出來很多底層的邏輯)是怎么做的
不管是java還是c語言繁成,要真正運(yùn)行吓笙,都會(huì)先變成匯編語言(這是可以看的),再變成機(jī)器碼(0,1無法看懂)
需要下載的工具:hsdis-amd64.dll
然后放到要使用的jdk下面的jre的bin目錄下面
然后巾腕,在idea配置項(xiàng)目啟動(dòng)的vm options
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VisualTest.prepareData
啟動(dòng)項(xiàng)目打印如下
可以看到有一lock字段的行面睛,其中這個(gè)對(duì)應(yīng)的是jvm的指令碼
這行匯編代碼做的就是對(duì)CPU高速緩存里面也就是工作內(nèi)存里面的變量的值進(jìn)行賦值,即assign操作尊搬,將修改的true賦值給工作內(nèi)存里面的變量
前面加了lock的匯編指令叁鉴,前綴指令,為匯編語言的關(guān)鍵字佛寿,一但進(jìn)行assign操作以后幌墓,會(huì)馬上接著執(zhí)行,cpu硬件會(huì)馬上將值同步回主內(nèi)存,不管線程2還有沒有其他代碼執(zhí)行
如果共享變量沒有加volatile克锣,那么前面不會(huì)有l(wèi)ock關(guān)鍵字茵肃,你不會(huì)知道他什么時(shí)候把值寫到主內(nèi)存,不會(huì)把改變馬上同步回主內(nèi)存袭祟,而是等線程2的其他代碼執(zhí)行完了再同步回主內(nèi)存
可能出現(xiàn)的問題點(diǎn)
數(shù)據(jù)進(jìn)入總線验残,通過mesi緩存一致性協(xié)議,將線程1工作內(nèi)存中的變量副本失效巾乳,但是可能存在一種情況:
線程2修改的數(shù)據(jù)在進(jìn)入總線還未同步到主內(nèi)存的時(shí)候您没,這時(shí)候initFlag還是false,線程1就從主內(nèi)存又去讀取胆绊,那么此時(shí)又將沒改變的數(shù)據(jù)讀取到了線程1的工作內(nèi)存
Volatile是怎么解決的氨鹏?
答案:volatile是加了一個(gè)緩存鎖,但是力度非常小压状,之前總線加鎖機(jī)制是將鎖加載read之前仆抵,但是volatile加鎖不是在read之前,而是在store之前
至于unlock是在store完种冬,write完再去unlock操作镣丑,這個(gè)操作實(shí)際上就是為了確保賦值,執(zhí)行時(shí)間非常非常少娱两,幾乎可以忽略莺匠,內(nèi)存中給變量賦值,少說也可以有幾十萬次十兢,甚至上百萬次
總結(jié):volatile實(shí)現(xiàn)由3重含義
- 馬上把數(shù)據(jù)同步回主內(nèi)存
- 觸發(fā)mesi緩存一致性協(xié)議讓本地內(nèi)存中變量失效
- 重新獲取的時(shí)候檢查主內(nèi)存中變量是否加鎖趣竣,沒有鎖的時(shí)候才
注意:常識(shí):兩個(gè)線程之間是不能直接交互的,必須通過主內(nèi)存旱物,不是說一個(gè)線程的修改能直接被另一個(gè)線程馬上看到遥缕,這個(gè)說法是錯(cuò)誤的,如下圖中的藍(lán)X
原子性案例
volatile只能保證可見性有序性异袄,不能保證原子性的原因
案例:
public class AtomicityTest {
public static volatile int num=0;
public static void increase(){
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i]=new Thread(new Runnable() {
@Override
public void run() {
for(int j=0;j<1000;j++)
increase();
}
});
threads[i].start();
}
for(Thread t:threads)
t.join();
System.out.println(num);
}
}
每次循環(huán)都new了一個(gè)線程通砍,讓他去執(zhí)行,每個(gè)線程里面都去做1000次for循環(huán)烤蜕,對(duì)共享變量num的值進(jìn)行++操作封孙,每個(gè)線程+1000次,10個(gè)線程執(zhí)行1w次,最后
打印num
結(jié)果是小于等于10000的
問題:為什么上面volatile沒起作用讽营,出現(xiàn)了小于10000的現(xiàn)象虎忌?
線程 1對(duì)num==0,進(jìn)行++操作以后assign操作將修改的1寫回到工作內(nèi)存橱鹏,還沒來得及寫到主內(nèi)存膜蠢,連總線都沒到的時(shí)候堪藐。線程2等不及了,也在cpu那邊對(duì)從工作內(nèi)存讀取到的num進(jìn)行++操作挑围,也將工作內(nèi)存中的值變成1了礁竞,那這個(gè)時(shí)候就會(huì)出現(xiàn)什么情況?
線程1將修改往主內(nèi)存寫的時(shí)候杉辙,通過總線的時(shí)候模捂,經(jīng)過mesi緩存一致性協(xié)議,將線程2的工作內(nèi)存中的變量num(此時(shí)的值為1了已經(jīng))的值設(shè)置為失效
這導(dǎo)致的嚴(yán)重后果就是:線程2之前的這個(gè)++操作已經(jīng)丟失了蜘矢,本來應(yīng)該等于2狂男,結(jié)果變成了1,
正常情況應(yīng)該是線程1的修改寫到主內(nèi)存品腹,線程2再進(jìn)行++操作岖食,這樣的話就沒問題
如果線程1能馬上寫回到主內(nèi)存,然后mesi協(xié)議清理掉線程2的值舞吭,然后線程2讀到1泡垃,才進(jìn)行++操作,可惜上面出問題是因?yàn)橄叟福€程2沒等到線程1的修改寫到主內(nèi)存的時(shí)候兔毙,線程2已經(jīng)進(jìn)行++操作
不能保證原子操作就是因?yàn)?+的操作可能存在丟失,所以會(huì)出現(xiàn)有時(shí)候是10000兄春,有時(shí)候小于10000