1. 概述
??隨著應(yīng)用程序訪問(wèn)量不斷增加,高并發(fā)是每一個(gè)應(yīng)用程序不得不面對(duì)的問(wèn)題。本文會(huì)從如下幾個(gè)層面來(lái)講述高并發(fā)相關(guān)的知識(shí)點(diǎn)
我們先看一個(gè)計(jì)數(shù)器的的簡(jiǎn)易實(shí)現(xiàn)好讓大家有個(gè)初步印象
@Slf4j
public class ConcurrencyCodeTest {
//請(qǐng)求總數(shù)
private final static Integer CLIENT_REQUEST_COUNT = 100;
//模擬20個(gè)線程
private final static Integer THREAD_TOTAL_COUNT = 20;
private static Integer count = 0;
public static void main(String [] args) throws InterruptedException {
//創(chuàng)建信號(hào)燈用于控制線程數(shù)
Semaphore semaphore = new Semaphore(THREAD_TOTAL_COUNT);
//創(chuàng)建CountDownLatch用于控制請(qǐng)求總數(shù)
CountDownLatch countDownLatch = new CountDownLatch(CLIENT_REQUEST_COUNT);
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_TOTAL_COUNT);
for(int i=0 ;i<CLIENT_REQUEST_COUNT ; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
log.info("線程{}",Thread.currentThread().getName() + " 開(kāi)始執(zhí)行");
add();
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
});
}
countDownLatch.await();
log.info("執(zhí)行完畢 當(dāng)前count {}",count);
}
private static void add(){
count = count + 1;
}
}
執(zhí)行多次我們會(huì)發(fā)現(xiàn)以上程序運(yùn)行結(jié)果并沒(méi)有達(dá)到我們的預(yù)期,具體原因我們后面再探討,大家可以好好思考下原因?
2. 基本概念
2.1 并發(fā)
??同時(shí)擁有兩個(gè)或者多個(gè)線程,如果程序在單核處理器上運(yùn)行宏赘,多個(gè)線程將交替的換入或者換出內(nèi)存,這些線程是同時(shí)"存在"的黎侈,每個(gè)線程都處于執(zhí)行過(guò)程中的某個(gè)狀態(tài)察署,如果運(yùn)行在多核處理器上,此時(shí)峻汉,程序中的每一個(gè)線程都將分配到一個(gè)處理器核上贴汪,因此可以同時(shí)運(yùn)行脐往。
??多個(gè)線程操作相同資源,保證線程安全扳埂,合理使用資源业簿。
2.2 高并發(fā)
??高并發(fā)(High Concurrency)是互聯(lián)網(wǎng)分布式架構(gòu)設(shè)計(jì)中必須考慮的因素之一,它通常是指聂喇,通過(guò)設(shè)計(jì)保證系統(tǒng)能夠同時(shí)并行處理很多請(qǐng)求辖源。
??服務(wù)能同時(shí)處理很多請(qǐng)求蔚携,提高程序性能希太。
2.3 CPU多級(jí)緩存與緩存一致性
2.3.1 為什么需要緩存
??cpu的頻率太快了,快到主存跟不上酝蜒,這樣在處理器時(shí)鐘周期內(nèi)誊辉,cpu常常需要等待主存,浪費(fèi)資源亡脑。cache的出現(xiàn)堕澄,是為了緩解cpu和主存之間速度的不匹配問(wèn)題(結(jié)構(gòu):cpu->cache>memory)。
2.3.2 緩存的意義
- 時(shí)間局部性:如果某個(gè)數(shù)據(jù)被訪問(wèn)霉咨,那么在不久的將來(lái)它很可能被再次訪問(wèn)蛙紫;
- 空間局部性:如果某個(gè)數(shù)據(jù)被訪問(wèn),那么與它相鄰的數(shù)據(jù)很快也可能被訪問(wèn)途戒;
2.3.3 緩存一致性(MESI)
MESI為了保證多個(gè)CPU緩存中共享數(shù)據(jù)的一致性坑傅,定義了cache line的四種狀態(tài),而CPU對(duì)cache line的四種操作可能會(huì)產(chǎn)生不一致的狀態(tài)喷斋,因此緩存控制器監(jiān)聽(tīng)到本地操作和遠(yuǎn)程操作的時(shí)候唁毒,需要對(duì)地址一致的cache line 狀態(tài)進(jìn)行一致性修改,從而保證數(shù)據(jù)在多個(gè)緩存之間保持一致性(M:modified E:Exclusive S:shared I:invalid) 星爪。
- 被修改(Modified): 該緩存行只被緩存在該CPU的緩存中浆西,并且是被修改過(guò)的(dirty),即與主存中的數(shù)據(jù)不一致,該緩存行中的內(nèi)存需要在未來(lái)的某個(gè)時(shí)間點(diǎn)(允許其它CPU讀取請(qǐng)主存中相應(yīng)內(nèi)存之前)寫(xiě)回(write back)主存顽腾。當(dāng)被寫(xiě)回主存之后近零,該緩存行的狀態(tài)會(huì)變成獨(dú)享(exclusive)狀態(tài)。
- 獨(dú)享的(Exclusive): 該緩存行只被緩存在該CPU的緩存中抄肖,它是未被修改過(guò)的(clean)秒赤,與主存中數(shù)據(jù)一致。該狀態(tài)可以在任何時(shí)刻當(dāng)有其它CPU讀取該內(nèi)存時(shí)變成共享狀態(tài)(shared)憎瘸。同樣地入篮,當(dāng)CPU修改該緩存行中內(nèi)容時(shí),該狀態(tài)可以變成Modified狀態(tài)幌甘。
- 共享的(Shared): 該狀態(tài)意味著該緩存行可能被多個(gè)CPU緩存潮售,并且各個(gè)緩存中的數(shù)據(jù)與主存數(shù)據(jù)一致(clean)痊项,當(dāng)有一個(gè)CPU修改該緩存行中,其它CPU中該緩存行可以被作廢(變成無(wú)效狀態(tài)(Invalid))酥诽。
- 無(wú)效的(Invalid): 該緩存是無(wú)效的(可能有其它CPU修改了該緩存行)鞍泉。
2.3.4 狀態(tài)轉(zhuǎn)換和Cache操作
- local read(LR):讀本地cache中的數(shù)據(jù);
- local write(LW):將數(shù)據(jù)寫(xiě)到本地cache肮帐;
- remote read(RR):其他核心發(fā)生read咖驮;
- remote write(RW):其他核心發(fā)生write;
初始場(chǎng)景:在最初的時(shí)候训枢,所有CPU中都沒(méi)有數(shù)據(jù)托修,某一個(gè)CPU發(fā)生讀操作,此時(shí)必然發(fā)生cache miss恒界,數(shù)據(jù)從主存中讀取到當(dāng)前CPU的cache睦刃,狀態(tài)為E(獨(dú)占,只有當(dāng)前CPU有數(shù)據(jù)十酣,且和主存一致)涩拙,此時(shí)如果有其他CPU也讀取數(shù)據(jù),則狀態(tài)修改為S(共享耸采,多個(gè)CPU之間擁有相同數(shù)據(jù)兴泥,并且和主存保持一致),如果其中某一個(gè)CPU發(fā)生數(shù)據(jù)修改虾宇,那么該CPU中數(shù)據(jù)狀態(tài)修改為M(擁有最新數(shù)據(jù)搓彻,和主存不一致,但是以當(dāng)前CPU中的為準(zhǔn))文留,其他擁有該數(shù)據(jù)的核心通過(guò)緩存控制器監(jiān)聽(tīng)到remote write行文好唯,然后將自己擁有的數(shù)據(jù)的cache line狀態(tài)修改為I(失效,和主存中的數(shù)據(jù)被認(rèn)為不一致燥翅,數(shù)據(jù)不可用應(yīng)該重新獲绕锔荨)。
modify
場(chǎng)景:當(dāng)前CPU中數(shù)據(jù)的狀態(tài)是modify森书,表示當(dāng)前CPU中擁有最新數(shù)據(jù)靶端,雖然主存中的數(shù)據(jù)和當(dāng)前CPU中的數(shù)據(jù)不一致,但是以當(dāng)前CPU中的數(shù)據(jù)為準(zhǔn)凛膏;
LR:此時(shí)如果發(fā)生local read杨名,即當(dāng)前CPU讀數(shù)據(jù),直接從cache中獲取數(shù)據(jù)猖毫,擁有最新數(shù)據(jù)台谍,因此狀態(tài)不變;
LW:直接修改本地cache數(shù)據(jù)吁断,修改后也是當(dāng)前CPU擁有最新數(shù)據(jù)趁蕊,因此狀態(tài)不變坞生;
RR:因?yàn)楸镜貎?nèi)存中有最新數(shù)據(jù),當(dāng)本地cache控制器監(jiān)聽(tīng)到總線上有RR發(fā)生的時(shí)掷伙,必然是其他CPU發(fā)生了讀主存的操作是己,此時(shí)為了保證一致性,當(dāng)前CPU應(yīng)該將數(shù)據(jù)寫(xiě)回主存任柜,而隨后的RR將會(huì)使得其他CPU和當(dāng)前CPU擁有共同的數(shù)據(jù)卒废,因此狀態(tài)修改為S;
RW:同RR宙地,當(dāng)cache控制器監(jiān)聽(tīng)到總線發(fā)生RW摔认,當(dāng)前CPU會(huì)將數(shù)據(jù)寫(xiě)回主存,因?yàn)殡S后的RW將會(huì)導(dǎo)致主存的數(shù)據(jù)修改绸栅,因此狀態(tài)修改成I级野;
exclusive
場(chǎng)景:當(dāng)前CPU中的數(shù)據(jù)狀態(tài)是exclusive页屠,表示當(dāng)前CPU獨(dú)占數(shù)據(jù)(其他CPU沒(méi)有數(shù)據(jù))粹胯,并且和主存的數(shù)據(jù)一致;
LR:從本地cache中直接獲取數(shù)據(jù)辰企,狀態(tài)不變风纠;
LW:修改本地cache中的數(shù)據(jù),狀態(tài)修改成M(因?yàn)槠渌鸆PU中并沒(méi)有該數(shù)據(jù)牢贸,因此不存在共享問(wèn)題竹观,不需要通知其他CPU修改cache line的狀態(tài)為I);
RR:本地cache中有最新數(shù)據(jù)潜索,當(dāng)cache控制器監(jiān)聽(tīng)到總線上發(fā)生RR的時(shí)候臭增,必然是其他CPU發(fā)生了讀取主存的操作,而RR操作不會(huì)導(dǎo)致數(shù)據(jù)修改竹习,因此兩個(gè)CPU中的數(shù)據(jù)和主存中的數(shù)據(jù)一致誊抛,此時(shí)cache line狀態(tài)修改為S;
RW:同RR整陌,當(dāng)cache控制器監(jiān)聽(tīng)到總線發(fā)生RW拗窃,發(fā)生其他CPU將最新數(shù)據(jù)寫(xiě)回到主存,此時(shí)為了保證緩存一致性泌辫,當(dāng)前CPU的數(shù)據(jù)狀態(tài)修改為I随夸;
shared
場(chǎng)景:當(dāng)前CPU中的數(shù)據(jù)狀態(tài)是shared,表示當(dāng)前CPU和其他CPU共享數(shù)據(jù)震放,且數(shù)據(jù)在多個(gè)CPU之間一致宾毒、多個(gè)CPU之間的數(shù)據(jù)和主存一致;
LR:直接從cache中讀取數(shù)據(jù)殿遂,狀態(tài)不變诈铛;
LW:發(fā)生本地寫(xiě)邪锌,并不會(huì)將數(shù)據(jù)立即寫(xiě)回主存,而是在稍后的一個(gè)時(shí)間再寫(xiě)回主存癌瘾,因此為了保證緩存一致性觅丰,當(dāng)前CPU的cache line狀態(tài)修改為M,并通知其他擁有該數(shù)據(jù)的CPU該數(shù)據(jù)失效妨退,其他CPU將cache line狀態(tài)修改為I妇萄;
RR:狀態(tài)不變,因?yàn)槎鄠€(gè)CPU中的數(shù)據(jù)和主存一致咬荷;
RW:當(dāng)監(jiān)聽(tīng)到總線發(fā)生了RW冠句,意味著其他CPU發(fā)生了寫(xiě)主存操作,此時(shí)本地cache中的數(shù)據(jù)既不是最新數(shù)據(jù)幸乒,和主存也不再一致懦底,因此當(dāng)前CPU的cache line狀態(tài)修改為I;
invalid
場(chǎng)景:當(dāng)前CPU中的數(shù)據(jù)狀態(tài)是invalid罕扎,表示當(dāng)前CPU中是臟數(shù)據(jù)聚唐,不可用,其他CPU可能有數(shù)據(jù)腔召、也可能沒(méi)有數(shù)據(jù)杆查;
LR:因?yàn)楫?dāng)前CPU的cache line數(shù)據(jù)不可用,因此會(huì)發(fā)生讀內(nèi)存臀蛛,此時(shí)的情形如下亲桦。
A. 如果其他CPU中無(wú)數(shù)據(jù)則狀態(tài)修改為E;
B. 如果其他CPU中有數(shù)據(jù)且狀態(tài)為S或E則狀態(tài)修改為S浊仆;
C. 如果其他CPU中有數(shù)據(jù)且狀態(tài)為M客峭,那么其他CPU首先發(fā)生RW將M狀態(tài)的數(shù)據(jù)寫(xiě)回主存并修改狀態(tài)為S,隨后當(dāng)前CPU讀取主存數(shù)據(jù)抡柿,也將狀態(tài)修改為S舔琅;
LW:因?yàn)楫?dāng)前CPU的cache line數(shù)據(jù)無(wú)效,因此發(fā)生LW會(huì)直接操作本地cache沙绝,此時(shí)的情形如下搏明。
A. 如果其他CPU中無(wú)數(shù)據(jù),則將本地cache line的狀態(tài)修改為M闪檬;
B. 如果其他CPU中有數(shù)據(jù)且狀態(tài)為S或E星著,則修改本地cache,通知其他CPU將數(shù)據(jù)修改為I粗悯,當(dāng)前CPU中的cache line狀態(tài)修改為M虚循;
C. 如果其他CPU中有數(shù)據(jù)且狀態(tài)為M,則其他CPU首先將數(shù)據(jù)寫(xiě)回主存,并將狀態(tài)修改為I横缔,當(dāng)前CPU中的cache line轉(zhuǎn)臺(tái)修改為M铺遂;
RR:監(jiān)聽(tīng)到總線發(fā)生RR操作,表示有其他CPU讀取內(nèi)存茎刚,和本地cache無(wú)關(guān)襟锐,狀態(tài)不變;
RW:監(jiān)聽(tīng)到總線發(fā)生RW操作膛锭,表示有其他CPU寫(xiě)主存粮坞,和本地cache無(wú)關(guān),狀態(tài)不變初狰;
2.4 Java內(nèi)存模型(Java Memory Mode,JMM)
??Java虛擬機(jī)規(guī)范中定義一種內(nèi)存模型來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致性的內(nèi)存訪問(wèn)效果莫杈。
Java內(nèi)存分為主內(nèi)存和工作內(nèi)存,主內(nèi)存主要對(duì)應(yīng)于Java堆中的對(duì)象實(shí)例數(shù)據(jù)部分奢入。而工作內(nèi)存則對(duì)應(yīng)于虛擬機(jī)棧中的部分區(qū)域筝闹。從更低的層次上說(shuō),主內(nèi)存就直接對(duì)應(yīng)于物理硬件的內(nèi)存腥光,而為了獲取更好的運(yùn)行速度关顷,虛擬機(jī)可能會(huì)讓工作內(nèi)存優(yōu)先存儲(chǔ)于寄存器和高速緩存中。
2.4.1 主柴我、工作內(nèi)存交互操作
??關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,Java內(nèi)存模型定義了8種操作來(lái)完成解寝。
- lock(鎖定):作用于主內(nèi)存的變量扩然,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)艘儒。
- unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái)夫偶,釋放后的變量才可以被其它線程鎖定界睁。
- read(讀取):作用于主內(nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存兵拢,以便隨后的load動(dòng)作使用翻斟。
- load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中说铃。
- use(使用):作用于工作內(nèi)存的變量访惜,它把工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作腻扇。
- assign(賦值):作用于工作內(nèi)存的變量债热,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作幼苛。
- store(存儲(chǔ)):作用于工作內(nèi)存的變量窒篱,它把工作內(nèi)存中一個(gè)變量的值傳遞到主內(nèi)存中,以便隨后的write操作使用。
- write(寫(xiě)入):作用于主內(nèi)存的變量墙杯,它把store操作從工作內(nèi)存中得到的變量值放入主內(nèi)存變量中配并。
??于此同時(shí),JMM還規(guī)定了在執(zhí)行上述8中操作時(shí)必須滿足如下規(guī)則
- 不允許read和load高镐、store和write操作之一單獨(dú)出現(xiàn)溉旋,即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者工作內(nèi)存發(fā)起回寫(xiě)了但主內(nèi)存不接受的情況出現(xiàn)嫉髓。
- 不允許一個(gè)線程丟棄它的最近的assign操作低滩,即變量在工作內(nèi)存中改變了之后必須把變化同步回主內(nèi)存。
- 不允許一個(gè)線程無(wú)原因地把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中岩喷。
- 一個(gè)新的變量只能在主內(nèi)存中"誕生"恕沫,不允許工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量,換句話說(shuō)纱意,就是對(duì)一個(gè)變量實(shí)施use婶溯、store操作之前,必須先執(zhí)行過(guò)assign和load操作偷霉。
- 一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作迄委,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后类少,只有執(zhí)行相同次數(shù)的unlock操作叙身,變量才會(huì)被解鎖。
- 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作硫狞,那將會(huì)清空工作內(nèi)存中此變量的值信轿,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值残吩。
- 如果一個(gè)變量事先沒(méi)有被lock操作鎖定财忽,那就不允許對(duì)它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其它線程鎖定住的變量泣侮。
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前即彪,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)