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