????????CPU械馆、內(nèi)存以及I/O設(shè)備都在不斷迭代胖眷,不斷朝著更快的方向努力。但是霹崎,在這個(gè)快速發(fā)展的過(guò)程中珊搀,有一個(gè)核心矛盾一直存在,即三者之間的速度差異尾菇。程序里大部分語(yǔ)句都要訪問(wèn)內(nèi)存境析,有些還要訪問(wèn)I/O,根據(jù)木桶原理派诬,程序整體的性能取決于最慢的操作——讀寫I/O設(shè)備劳淆,也就是說(shuō)單方面提高CPU性能是無(wú)效的。為了合理利用CPU的高性能默赂,平衡這三者的速度差異沛鸵,計(jì)算機(jī)體系機(jī)構(gòu)、操作系統(tǒng)缆八、編譯程序都做出了貢獻(xiàn)曲掰,主要體現(xiàn)為:
? ? ? ? 1、CPU增加了緩存奈辰,以均衡與內(nèi)存的速度差異栏妖;
? ? ? ? 2、操作系統(tǒng)增加了進(jìn)程奖恰、線程吊趾,以分時(shí)復(fù)用CPU宛裕,進(jìn)而均衡CPU與I/O設(shè)備的速度差異;
? ? ? ? 3论泛、編譯程序優(yōu)化指令執(zhí)行次序续滋,使得緩存能夠得到更加合理地利用。
增加緩存孵奶,帶來(lái)了可見(jiàn)性的問(wèn)題
? ? ? ? 在單核時(shí)代,所有的線程都是在一顆CPU上執(zhí)行蜡峰,CPU緩存與內(nèi)存的數(shù)據(jù)一致性很容易解決了袁,因?yàn)樗械木€程都在同一個(gè)CPU運(yùn)行,即操作的都是同一個(gè)CPU的內(nèi)存湿颅,故一個(gè)線程對(duì)緩存的寫载绿,對(duì)另外一個(gè)線程來(lái)說(shuō)一定是可見(jiàn)的。如圖:
? ? ? ? 由圖可知油航,當(dāng)線程A更新共享變量V的值崭庸,那么線程B之后再訪問(wèn)變量V,得到一定是V的最新值谊囚。
? ? ? ? 多核時(shí)代怕享,每個(gè)CPU都有各自的緩存,當(dāng)多個(gè)線程在不同的CPU上執(zhí)行時(shí)镰踏,這些線程操作的是不同的CPU緩存函筋,故可見(jiàn)性就不是那么容易保證了。
? ? ? ? 一個(gè)線程對(duì)共享變量的修改奠伪,另外一個(gè)線程能夠立刻看到跌帐,稱為可見(jiàn)性。
線程切換绊率,帶來(lái)原子性問(wèn)題
? ? ? ? 操作系統(tǒng)允許某個(gè)進(jìn)程執(zhí)行一小段時(shí)間谨敛,過(guò)了這一小段時(shí)間,操作系統(tǒng)就會(huì)重新選擇一個(gè)進(jìn)程來(lái)執(zhí)行(任務(wù)切換)滤否,這小段時(shí)間稱為”時(shí)間片“脸狸。在一個(gè)時(shí)間片內(nèi),如果一個(gè)進(jìn)程進(jìn)行一個(gè)IO操作顽聂,例如讀個(gè)文件肥惭,這個(gè)時(shí)候該進(jìn)程可以把自己標(biāo)記為”休眠狀態(tài)“并出讓CPU的使用權(quán),待文件讀進(jìn)內(nèi)存紊搪,操作系統(tǒng)會(huì)把這個(gè)休眠的進(jìn)程喚醒蜜葱,喚醒后的進(jìn)程就有機(jī)會(huì)重新獲得CPU的使用權(quán)了。進(jìn)程在等待IO時(shí)之所以會(huì)釋放CPU使用權(quán)耀石,是為了讓CPU在這段時(shí)間里可以做別的事情牵囤,這樣一來(lái)CPU的使用率就上來(lái)了爸黄;此外,如果這時(shí)有另外一個(gè)進(jìn)程也讀文件揭鳞,讀文件的操作就會(huì)排隊(duì)炕贵,磁盤驅(qū)動(dòng)在完成一個(gè)進(jìn)程的讀操作后,發(fā)現(xiàn)有排隊(duì)的任務(wù)野崇,就會(huì)立即啟動(dòng)下一個(gè)讀操作称开,這樣IO的使用率也上來(lái)了。
? ? ? ? 早期的操作系統(tǒng)基于進(jìn)程來(lái)調(diào)度CPU乓梨,不同進(jìn)程間是不共享內(nèi)存空間的鳖轰,所以進(jìn)程要做任務(wù)切換就要切換內(nèi)存映射地址,而一個(gè)進(jìn)程創(chuàng)建的所有線程扶镀,都是共享一個(gè)內(nèi)存空間的蕴侣,所以線程做任務(wù)切換成本就很低了。現(xiàn)代的操作系統(tǒng)都基于更輕量的線程來(lái)調(diào)度臭觉,現(xiàn)在提到的”任務(wù)切換“都是指”線程切換“昆雀。
? ? ? ? Java并發(fā)程序都是基于多線程的,自然會(huì)涉及到任務(wù)切換蝠筑,任務(wù)切換是并發(fā)編程里詭異Bug的源頭之一狞膘。任務(wù)切換的時(shí)機(jī)大多數(shù)是在時(shí)間片結(jié)束的時(shí)候,現(xiàn)在基本都是使用高級(jí)語(yǔ)言編程什乙,高級(jí)語(yǔ)言一條語(yǔ)句往往需要多條CPU指令完成客冈,例如count++,至少需要三條cpu指令來(lái)完成稳强。操作系統(tǒng)做任務(wù)切換场仲,可以發(fā)生在任何一條CPU指令執(zhí)行完,注意退疫,不是高級(jí)語(yǔ)言里的一條語(yǔ)句渠缕。
? ? ? ? 所謂的原子性,就是指一個(gè)或者多個(gè)操作在CPU執(zhí)行的過(guò)程中不被中斷的特性褒繁。CPU能保證的原子操作是CPU指令級(jí)別的亦鳞,而不是高級(jí)語(yǔ)言的操作符。
編譯優(yōu)化棒坏,帶來(lái)了有序性問(wèn)題
? ? ? ? 有序性燕差,指的是程序按照代碼的先后順序執(zhí)行。編譯器為了優(yōu)化性能坝冕,有時(shí)候會(huì)改變程序中語(yǔ)句的先后順序徒探,但是不影響程序的最終結(jié)果。不過(guò)有時(shí)候編譯器及解釋器的優(yōu)化可能導(dǎo)致意想不到的Bug喂窟。例如:雙重檢查再加鎖的方式實(shí)現(xiàn)的單例模式测暗。
????????public class Singleton {
? ? ? ? ????????static Singleton instance;
? ? ? ????????? static Singleton getInstance() {
? ? ? ? ? ? ? ? ????????if(instance? ? ==? ? null) {
? ? ? ? ? ? ? ? ? ? ? ? ????????synchronized(Singleton.class) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ????????? if(instance? ? ==? ? null) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ????????instance? ? =? ? new Singleton();
????????????????????????????????????????}
????????????????????????????????}
? ? ? ? ? ? ? ? ????????return instance;
? ? ? ? ? ? ? ? ? }
?????????}
? ? ? ? 假設(shè)有兩個(gè)線程A央串、B同時(shí)調(diào)用getInstance()方法,他們會(huì)同時(shí)發(fā)現(xiàn)instance == null碗啄,于是同時(shí)對(duì)Singleton加鎖质和,此時(shí)JVM保證只有一個(gè)線程能夠加鎖成功,假設(shè)線程A加鎖成功稚字,線程B處于等待狀態(tài)饲宿。線程A會(huì)創(chuàng)建一個(gè)Sington實(shí)例,之后釋放鎖胆描,鎖釋放后褒傅,線程B被喚醒,線程B再次嘗試加鎖袄友,此時(shí)是可以加鎖成功的,加鎖成功后霹菊,線程B檢查instance == null時(shí)會(huì)發(fā)現(xiàn)剧蚣,已經(jīng)創(chuàng)建過(guò)Singleton實(shí)例了,所以線程B不會(huì)在創(chuàng)建一個(gè)Singleton實(shí)例旋廷。
? ? ? ? 這個(gè)過(guò)程看上去沒(méi)什么問(wèn)題鸠按,但是問(wèn)題出在new操作上,new操作分為如下的步驟:
? ? ? ? ? ? ? ? 1饶碘、分配一個(gè)內(nèi)存M目尖;
? ? ? ? ? ? ? ? 2、在內(nèi)存M上初始化Singleton對(duì)象扎运;
? ? ? ? ? ? ? ? 3瑟曲、然后M的地址賦值給instant變量;
? ? ? ? 但是經(jīng)過(guò)編譯器優(yōu)化后豪治,可能new操作的執(zhí)行順序可能會(huì)變成這樣:1->3->2洞拨,當(dāng)先執(zhí)行的線程執(zhí)行至3時(shí),恰好發(fā)生了線程切換负拟,切換到別的線程烦衣,別的線程執(zhí)行到了第一個(gè)if語(yǔ)句,此時(shí)判斷結(jié)果為false掩浙,直接返回instance實(shí)例花吟,而instance實(shí)例還沒(méi)有初始化,訪問(wèn)instance實(shí)例的時(shí)候會(huì)出現(xiàn)空指針異常厨姚。