java并发执行一个方法_JAVA的执行并发原理
Volatile
Volatile關(guān)鍵字用于確保共享數(shù)據(jù)的可見性與有序性,但是并不能保證方法的原子性,在程序中對(duì)Volatile關(guān)鍵字使用得當(dāng)?shù)脑?#xff0c;它比synchronized的使用和執(zhí)行成本會(huì)更低,因?yàn)樗粫?huì)引起線程的上下文切換和調(diào)度。
先講一下重排序,重排序是什么?
我們所編寫的程序會(huì)經(jīng)過(guò)編譯器編譯,然后寫入內(nèi)存中。在執(zhí)行時(shí),CPU會(huì)從內(nèi)存中讀取并執(zhí)行,在這里,編譯器與CPU為了提高程序執(zhí)行時(shí)的效率,會(huì)對(duì)代碼的執(zhí)行順序進(jìn)行優(yōu)化,但代碼輸出的結(jié)果并不會(huì)改變,所以從宏觀上我們認(rèn)為程序是按照我們的思路來(lái)運(yùn)行的,這里有三種重排序:
1.編譯器重排序:在不改變代碼語(yǔ)義的情況下,對(duì)重新對(duì)代碼執(zhí)行順序進(jìn)行排序。
2.CPU重排序:我們的代碼在CPU處理時(shí),會(huì)被編譯成各種指令,若不存在數(shù)據(jù)依賴性,處理器在執(zhí)行代碼語(yǔ)句時(shí),可以對(duì)其生成的指令進(jìn)行重排序。
3.緩存的重排序:在CPU對(duì)緩存進(jìn)行讀/寫時(shí),加載與存儲(chǔ)的操作是存在亂序的。
所以在單線程運(yùn)行情況下的,重排序是提升程序的執(zhí)行效率,這些重排序?qū)Τ绦蜻\(yùn)行是無(wú)害的,而在多線程運(yùn)行情況下,線程交替執(zhí)行則會(huì)出問(wèn)題。先看一個(gè)比較常見的例子:
class Counter{public static int count = 0;public static boolean flag = false;public void inc(){ count++; //-------操作1 flag = true; //-------操作2}public int getCount(){ if(falg){ flag = false; //-------操作3 return count; //-------操作4 } return 0;}
正常情況下是調(diào)用inc()后執(zhí)行操作1與操作2,然后再調(diào)用getCount()執(zhí)行操作3與操作4,但是再多線程情況下,如果對(duì)這段代碼進(jìn)行了重排序,很有可能會(huì)出現(xiàn)如下結(jié)果:
1.線程A調(diào)用了inc(),代碼被重排序后執(zhí)行順序?yàn)橄葘lag置為ture,再對(duì)count進(jìn)行自增。
2.而此時(shí)線程B調(diào)用了getCount(),此時(shí)線程A只運(yùn)行了flag置為ture,還沒(méi)有執(zhí)行到對(duì)count自增,這時(shí)線程B就會(huì)返回非預(yù)期值。
在我們使用Volatile關(guān)鍵字時(shí),JMM會(huì)向CPU指令中插入特定的指令來(lái)確保共享數(shù)據(jù)可見性與有序性。
1.內(nèi)存屏障用于保障有序性
內(nèi)存屏障是一組同步指令集,使得CPU與編譯器在對(duì)加入內(nèi)存屏障之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點(diǎn)之后的操作。它用于保障程序的有序執(zhí)行,被插入內(nèi)存屏障指令的代碼,會(huì)對(duì)其實(shí)際代碼執(zhí)行順序進(jìn)行限制,有以下四種內(nèi)存屏障:
根據(jù)JSR-133 CookBook中的描述,我們可以從下表中得出結(jié)論:
1.如果第二個(gè)操作是Volatile寫,那么無(wú)論第一個(gè)操作是什么類型的操作,都不能改變代碼的執(zhí)行順序。它用于保障Volatile寫之前的操作不會(huì)被重排序到其后執(zhí)行。
2.如果第一個(gè)操作是Volatile讀,那么無(wú)論第二個(gè)操作是什么類型的操作,都不能改變代碼的執(zhí)行順序。它用于保障Volatile讀之后的操作不會(huì)被沖排序到其前面執(zhí)行。
為了達(dá)到上述規(guī)則,編譯器在生成字節(jié)碼時(shí),會(huì)在指令中插入內(nèi)存屏障來(lái)保證Volatile數(shù)據(jù)前后代碼的執(zhí)行順序。
讀:
1.在對(duì)一個(gè)Volatile讀操作之后添加一個(gè)LoadLoad指令。(用來(lái)確保當(dāng)前讀操作之后的讀操作都不會(huì)被重排序)
2.對(duì)于一個(gè)Volatile讀操作之后添加一個(gè)LoadStore指令。(用來(lái)確保當(dāng)前讀操作于后續(xù)寫操作之前進(jìn)行)
寫:
1.在對(duì)一個(gè)Volatile寫操作之前添加一個(gè)StoreStore指令。(用來(lái)確保當(dāng)前寫操作之前的寫操作不會(huì)被重排序到其后面執(zhí)行)
2.在一個(gè)Volatile寫操作之后添加一個(gè)StoreLoad指令。(用來(lái)確保當(dāng)前寫操作于之后讀操作之前執(zhí)行)
2.可見性
當(dāng)共享變量被Volatile關(guān)鍵字修飾后,對(duì)Volatile變量進(jìn)行讀寫都使JVM在變量前插入LOCK前綴指令,若不涉及緩存一致性協(xié)議,LOCK前綴指令會(huì)鎖住總線,使其他CPU暫時(shí)無(wú)法通過(guò)總線訪問(wèn)內(nèi)存,作用如下:
1.對(duì)被Volatile關(guān)鍵字修飾的變量進(jìn)行寫操作,Lock指令會(huì)直接將工作內(nèi)存中的變量刷新至主存中。
1.對(duì)于被Volatile關(guān)鍵字修飾的變量進(jìn)行讀操作,Lock指令會(huì)令工作內(nèi)存中的該變量失效,直接從主存中讀取。
這里需要注意的是Volatile關(guān)鍵字只能修飾單個(gè)變量,它無(wú)法保障代碼塊的原子性,如a++這樣的操作并不能保障它的原子性,因?yàn)樗怯勺鰝€(gè)指令集組成。
volitatile的使用情景:
1.狀態(tài)標(biāo)記
2.double check
synchronized
synchronized關(guān)鍵字用于解決在并發(fā)編程時(shí)的有序性、原子性、可見性。相比于Volatile關(guān)鍵字,synchronized鎖能夠控制的范圍更大,使用synchronized關(guān)鍵字修飾方法或代碼塊時(shí),能夠確保在同一時(shí)刻最多只有一個(gè)線程能夠執(zhí)行該代碼。當(dāng)synchronized修飾方法時(shí),它鎖住的是對(duì)象的實(shí)例synchronized(this),當(dāng)作用在對(duì)象實(shí)例時(shí),它鎖住的是代碼塊。
synchronized實(shí)現(xiàn)原理:
我們對(duì)于synchronized的理解也許只在于其互斥的特性,認(rèn)為線程在執(zhí)行加上synchronized關(guān)鍵字的代碼,需要執(zhí)行該代碼塊或方法的線程就會(huì)競(jìng)爭(zhēng)該代碼塊或方法的互斥鎖。若競(jìng)爭(zhēng)成功則線程該代碼塊的執(zhí)行權(quán),而未競(jìng)爭(zhēng)到該鎖的線程只能被阻塞,等到持有鎖的線程執(zhí)行代碼塊或方法執(zhí)行完畢后釋放鎖,其他線程才能繼續(xù)競(jìng)爭(zhēng),而這僅僅synchronized關(guān)鍵字中重量鎖的特性。其實(shí)synchronized在經(jīng)過(guò)不斷優(yōu)化后,其鎖的特性為偏向鎖-》輕量鎖-》重量鎖三種,偏量鎖和輕量鎖在某種意義上能夠減少重量鎖帶來(lái)的開銷,但是他們都不能替代重量鎖,下面就讓我們來(lái)看看這三種鎖的原理。
重量鎖
重量鎖故名思議就是需要消耗大量系統(tǒng)資源的鎖,因?yàn)楹苤芈铩.?dāng)多線程執(zhí)行到具有synchronized關(guān)鍵字的代碼時(shí),會(huì)進(jìn)行鎖競(jìng)爭(zhēng),競(jìng)爭(zhēng)失敗的線程會(huì)進(jìn)入一個(gè)阻塞隊(duì)列,而獲得鎖的線程會(huì)獲取代碼的執(zhí)行權(quán)。
而synchronized鎖是一種非公平鎖,當(dāng)線程競(jìng)爭(zhēng)失敗時(shí)會(huì)阻塞,我們知道線程從運(yùn)行狀態(tài)切換到阻塞狀態(tài)是依賴于操作系統(tǒng)從用戶態(tài)切換到內(nèi)核態(tài)來(lái)執(zhí)行的,這種切換會(huì)消耗大量的系統(tǒng)資源(因?yàn)橛脩魬B(tài)與內(nèi)核態(tài)都有各自專用的內(nèi)存空間,專用的寄存器租等,用戶態(tài)切換至內(nèi)核態(tài)需要傳遞給許多變量、參數(shù)給內(nèi)核,內(nèi)核也需要保護(hù)好用戶態(tài)在切換時(shí)的一些寄存器值、變量等,以便內(nèi)核態(tài)調(diào)用結(jié)束后切換回用戶態(tài)繼續(xù)工作)。如果該方法是一個(gè)高頻操作時(shí),這將會(huì)消耗很多CPU處理時(shí)間。所以為了避免線程阻塞帶來(lái)的消耗,引入了輕量鎖。
輕量鎖
輕量鎖是為了避免在沒(méi)有競(jìng)爭(zhēng)的情況下重量鎖所帶來(lái)的開銷,一旦該對(duì)象有多個(gè)線程競(jìng)爭(zhēng),輕量鎖就會(huì)升級(jí)為重量鎖,所以輕量鎖和偏向鎖并不能在多線程競(jìng)爭(zhēng)情況下代替重量鎖!!!只能在無(wú)鎖競(jìng)爭(zhēng)的條件下減緩重量鎖的開銷。在了解輕量鎖前,我們先了解一下CAS操作與mark word標(biāo)記,他們是實(shí)現(xiàn)輕量鎖的基礎(chǔ)。
CAS
CAS英文名(compare and swap)也就是比較交換,java語(yǔ)言在代碼層面對(duì)其進(jìn)行了封裝,實(shí)際上是它是通過(guò)調(diào)用jni來(lái)實(shí)現(xiàn)的,它本質(zhì)上是調(diào)用了cpu的指令集。在JUC中大量的使用到了CAS操作,它作為一種樂(lè)觀鎖,使用它就能實(shí)現(xiàn)所謂的無(wú)鎖交換。
在CAS操作中,一個(gè)變量有三個(gè)狀態(tài)值,一個(gè)是內(nèi)存值V,一個(gè)是舊的預(yù)期值A(chǔ),還有一個(gè)是要替換新值的B。對(duì)應(yīng)到JMM中,V表示為主存中的值,而舊的預(yù)期值A(chǔ)為我們工作內(nèi)存中的值,而B為操作后的新值,若V與B相等就說(shuō)明該變量沒(méi)有被其他線程修改,那么將變量替換為B值,不相等則不進(jìn)行交換。所以使用CAS操作的開銷相對(duì)于線程競(jìng)爭(zhēng)過(guò)程中的阻塞喚醒引起的上下文切換來(lái)說(shuō)小了很多(競(jìng)爭(zhēng)情況下,操作隊(duì)列,線程掛起,上下文切換)。
MARK WORD
我們知道java的Class文件是對(duì)java程序二進(jìn)制文件格式的定義,java編譯器將Class文件編譯成字節(jié)碼在jvm中運(yùn)行,在堆內(nèi)存中的對(duì)象都含有各自的對(duì)象頭用于確定obj在運(yùn)行時(shí)的狀態(tài),而Mark Word正是一個(gè)長(zhǎng)為32bit的對(duì)象頭,是用來(lái)標(biāo)記同步線程的
這里先解釋HashCode 與state兩個(gè)變量的值,輕量鎖與重量鎖在HashCode中存入的值為指向占有鎖的線程的棧中的存儲(chǔ)該線程所占有鎖的信息的地址(有點(diǎn)繞口,其實(shí)就是存了一個(gè)地址,下面會(huì)詳細(xì)說(shuō)),state表示當(dāng)前對(duì)象所處的狀態(tài),下面我們來(lái)看一下輕量鎖如何來(lái)實(shí)現(xiàn)鎖機(jī)制,這里分兩種情況。
該對(duì)象沒(méi)有被其他線程鎖定
因?yàn)檩p量鎖是由偏向鎖升級(jí)而來(lái),通過(guò)判斷tag是否為1與鎖標(biāo)志位是否為01來(lái)得知對(duì)象是否有沒(méi)有被其他線程占用,若沒(méi)有被其他線程占用,jvm會(huì)在當(dāng)前線程的棧中創(chuàng)建一個(gè)lock record空間,將當(dāng)前需要被鎖定的對(duì)象的mark word的拷貝副本存入到lock record中,然后嘗試使用CAS操作將mark Word中的betifields字段中的值更新為指向lock record空間的地址,若CAS操作成功,則將state更新為00,表示輕量鎖添加成功,當(dāng)前線程擁有對(duì)該對(duì)象的執(zhí)行權(quán)。
2.出現(xiàn)鎖競(jìng)爭(zhēng)或?qū)ο笠呀?jīng)被其他線程占用
若有兩個(gè)線程同時(shí)對(duì)未被鎖定的對(duì)象上輕量鎖時(shí),會(huì)有一個(gè)線程競(jìng)爭(zhēng)失敗,此時(shí)競(jìng)爭(zhēng)失敗標(biāo)志是CAS操作失敗,則該線程會(huì)自旋一段時(shí)間,若還是CAS操作失敗,則該輕量鎖會(huì)升級(jí)為重量鎖,競(jìng)爭(zhēng)失敗的線程進(jìn)入阻塞狀態(tài),state標(biāo)志置為10,且mark down中重量鎖指針會(huì)被修改。當(dāng)占有該輕量鎖的線程釋放鎖時(shí),競(jìng)爭(zhēng)失敗的鎖會(huì)被喚醒,重新競(jìng)爭(zhēng)鎖。
2.unlock
解鎖過(guò)程也是將lock record中存儲(chǔ)的mark down副本與object頭中mark down進(jìn)行CAS操作,若兩者相等,則說(shuō)明沒(méi)有其他線程競(jìng)爭(zhēng)該輕量鎖,釋放成功。如果失敗,則當(dāng)前輕量鎖存在競(jìng)爭(zhēng),則鎖會(huì)升級(jí)為重量鎖。
從上述輕量鎖實(shí)現(xiàn)過(guò)程我們可以看到輕量鎖是使用CAS操作來(lái)代替重量鎖的互斥操作,在語(yǔ)言層面上實(shí)現(xiàn)了同步操作,這樣能夠節(jié)約許多系統(tǒng)開銷,但是需要注意的是,這都是在無(wú)鎖競(jìng)爭(zhēng)的前提條件下,因?yàn)檩p量鎖并不能代替重量鎖。
偏向鎖
偏向鎖是在JVM1.6中引入了,主要也是為了解決在沒(méi)有競(jìng)爭(zhēng)情況下鎖性能的問(wèn)題,通過(guò)上述輕量鎖的講解,我們了解到輕量鎖是通過(guò)CAS操作來(lái)避免重量鎖的阻塞開銷。但是我們知道CAS操作也是需要通過(guò)本地調(diào)用來(lái)實(shí)現(xiàn),歸根到底還是通過(guò)CPU指令集的實(shí)現(xiàn),JVM只是封裝了該指令調(diào)用。所以CAS操作會(huì)產(chǎn)生一定的副作用。因?yàn)镃PU通過(guò)總線來(lái)實(shí)現(xiàn)對(duì)內(nèi)存中數(shù)據(jù)的讀寫,而多核CPU在將自身cache內(nèi)存中的數(shù)據(jù)刷新至主存中時(shí),會(huì)引觸發(fā)“緩存一致性協(xié)議”,就是說(shuō)CPU1對(duì)主存中的值進(jìn)行了改變,“緩存一致性協(xié)議”會(huì)通知CPU2、CPU3自身cache中該值已經(jīng)失效,需要重新讀取。若在輕量鎖中每次進(jìn)入操作,若CAS操作很頻繁的話,會(huì)給總線帶來(lái)巨大的開銷,而偏向鎖就是為了避免這個(gè)開銷產(chǎn)生的。
若線程在沒(méi)有競(jìng)爭(zhēng)的情況下去獲取某一對(duì)象的鎖,會(huì)通過(guò)CAS操作將自身的Thread ID 存入Mark Word中,如果CAS操作成功,則表示該線程擁有該對(duì)象的執(zhí)行權(quán),而偏向鎖是具有可重入性的,偏向嗎,就是偏袒第一次占有該對(duì)象鎖的線程,當(dāng)該線程再次競(jìng)爭(zhēng)該對(duì)象的鎖時(shí),只需要對(duì)比較Mark Word中的Thread ID 與自身的Thread ID 是否相同,相同則表明沒(méi)有其他線程競(jìng)爭(zhēng),可以繼續(xù)使用;如果這時(shí)有線程來(lái)競(jìng)爭(zhēng),則該線程在執(zhí)行完代碼塊后,偏向鎖會(huì)升級(jí)為輕量鎖。這里需要注意的是,偏向鎖只有再有競(jìng)爭(zhēng)時(shí)才會(huì)撤銷,若沒(méi)有競(jìng)爭(zhēng),則一直是第一次獲得偏向鎖的線程持有。我們可以看到偏向鎖的出現(xiàn)更加降低了線程初次獲取鎖的開銷。
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的java并发执行一个方法_JAVA的执行并发原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: mysql与文件_MySQL——文件
- 下一篇: 一个服务器上放多个网站,一个云服务器放多